小引入
如果一个系统由n个变量和m个约束条件组成,形成如xi-xj<=k(ij∈[1, n])的不等式,则称其为查分约束系统。写成一个矩阵形式的不等式组的话则系数有且仅有一个1和一个-1。
解这种不等式组为何会与图论扯上关系呢?在我们求解最短路或者是最长路时会有松弛操作
if(dis[v] > dis[u] + w) { // 求解最短路,移相后为dis[v] - dis[u] > w
dis[v] = dis[u] + w; // 求出的dis[v] 满足 dis[v] - dis[u] <= w
...
}
if(dis[v] < dis[u] + w) { // 求解最长路,移相后为dis[v] - dis[u] < w
dis[v] = dis[u] + w; // 求出的dis[v]满足 dis[v] - dis[u] >= w
...
}
可以发现在松弛操作移相之后我们看到式子变成了关于dis[i]的多元不等式方程且系数只有1和-1,符合我们的查分约束系统。通过注释的分析我们可以看到第一个条件松弛球的是最短路,求出的dis[v]是满足方程的dis[v]-dis[u] <= w的最大解,因为dis每次取得都是<=中得=号,相当于取了上界——所以对于x1-x2<=w的不等式组我们可以转换为求最短路来求出满足条件的最大解。同理对于x1-x2>=w的不等式组我们可以转换为求最长路来求出满足所有约束条件的最小解。这里要注意不等式方程要满足等号不能严格的大于小于。and,差分约束是用来求解方程的,所以最初始的任务先是要确定未知数是什么,未知数若没有事先明确,很容易会混淆自己到底在求什么。未知数的选取往往还关系到最后能否成功求解,
由于此时的边权可能会涉及到负数--所以不能使用dijkstra,只能用bellman-ford或者spfa,因为spfa其实就是bellman-ford的队列优化版,一般直接考虑使用spfa就好了。下面通过例题来练一练。
就题论事
题目:XXZZY hdu1317
基本大意:一张n个点的图,你初始有100的基础值,每到一个点就加上该点的权值(可正可负),问在自身能量不耗尽的情况下是否能到达终点。
哈~没有错,这道题跟查分约束没有什么关系,只是先来对spfa练练手~若图中存在正环则必能赢--所以一共有两种可以获胜的情况:存在正环,不需正环就直接到达。
这里我想提一下判断负环正环的原理(这里单举负环,正环是一样的):bellman-ford算法是通过不断地循环遍历所有边来找到最短路,第一次遍历所有边就可以找到通过一条边到源点的最短路,第二次遍历就可以找到通过两条边到源点的最短路……由于n个点的最短路最多可能有n-1条边,所以最多遍历n-1次就可以确定最短路,注意这里是最多,所以为了提高效率往往在每一次遍历完边后会判断一下该次遍历是由进行了松弛操作,若没有进行松弛操作则说明最短路已经确定可以直接结束循环。若进行了n-1次仍然还能够进行松弛则说明存在负环,不可能找到最短路。
/* 注意这个只是单纯的伪码哦~ */
bool keep = true;
for(int i=0; i<n-1; ++i) {
for(int e=1; e<=m; ++e) {
if(dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
keep = false;
}
}if(!keep) break; // 该轮遍历没有进行松弛, 已经确定了最短路
}
for(int e=1; e<=m; ++e) // 再遍历一次所有边检查是否还能松弛
if(dis[v] > dis[u] + w)
...存在负环...
spfa则是bellman-ford的队列优化,优化在于更新了某一点v的最短路后只继续遍历与v相连的所有边而不是每次都暴力遍历所有边。对比原本的bellman-ford我们可以发现其实bellman-ford的每一暴力枚举所有边能进行松弛的其实也都是与更新过的点相连的边。
使用了队列就变成了层次的遍历,所以在spfa中每次点v入队其实就是到v有经过更多边的一条路,BECAUSE每一次再入队时v所在的层次一定比它上一次高!那么在最坏的情况下,假设每次v入队所在的层次边只增加了一条,那么在有n个点的图中由于最短路最多只可能有n-1条边,所以v的入队次数最多只有n-1次,如果入队次数超过了n-1次那么必然存在负环始得最短路不存在!
所以用spfa时判断是否存在负环的办法是记录算法过程中每个点的入队次数,(入队次数)>(总点数-1)则存在负环。注意有一个大前提-->图要连通。对于最长路判断正环同理可得~
/* 求最短路为例子
while循环的可靠性:因为spfa在每次循环是不断松弛,松弛成功才入队
所以若存在最短路则必然有松弛完的时候,否则存在负环
*/
struct Edge { // 链式向前星存图
int to, w, nxt;
}e[M];
int hd[N], cnte, dis[N], inq[N], cn[N];
void spfa(const int& s) {
memset(dis, 0x3f, sizeof(dis)),
memset(inq, false, sizeof(inq)), // bool inq[N]数组判断某点是否已经在队列中,如果已经在队列中则没有必要再入队
memset(cn, 0, sizeof(cn)); // int cn[N]数组统计每点的入队次数
queue<int> q; while(q.size()) q.pop();
dis[s] = 0;
q.push(s), inq[s] = true;
while(q.size()) {
int u = q.front();
q.pop(), inq[u] = false;
for(int i=hd[u]; i; i=e[i].nxt) { // 遍历该点的所有出边
int v = e[i].to;
if(dis[v] > dis[u] + e[i].w) {
dis[v] = dis[u] + e[i].w;
if(!inq[v]) { // 不在队中则将该点入队
if(++cn[v] > n-1) { ...存在负环, 无最短路... return; }
q.push(v), inq[v] = true;
}
}
}
}return; // 求最短路成功
}
好啦是时候给出这道题的题解啦
/*
hdu 1317
@auther Bankarian
tips: 求最长路时, dis初始化为-oo
求最短路时, dis初始化为+oo, 注意不要混啦
*/
#include <cstdio>
#include <cstring>
#include <queue>
#define Inf 0x3f3f3f3f
using namespace std;
const int N = 150;
bool can[N][N];
struct Edge {
int to, nxt;
}e[N*100]; // 最多每个点与100个点相连,边数max=n*100
int hd[N], cnte;
int dis[N], cn[N], w[N];
bool inq[N];
int n;
inline void add_edge(const int& from, const int& to) {
++cnte;
e[cnte].to = to, e[cnte].nxt = hd[from];
hd[from] = cnte;
return;
}
inline bool floyd() { // 传递闭包预处理
for(int k=1; k<=n; ++k)
for(int i=1; i<=n; ++i)
for(int j=1; j<=n; ++j)
can[i][j] |= can[i][k] & can[k][j];
return can[1][n];
}
bool spfa() {
queue<int> q; while(q.size()) q.pop();
q.push(1);
dis[1] = 100, inq[1] = true, cn[1] = 1;
while(q.size()) {
int u = q.front();
q.pop(), inq[u] = false;
if(u == n) return true;
for(int i=hd[u]; i; i=e[i].nxt) {
int v = e[i].to;
int tmp = dis[u] + w[v];
if(tmp > 0 && dis[v] < tmp && (can[v][n] || v == n)) { // 找正环即找最长路
dis[v] = tmp;
if(!inq[v]) {
if(++cn[v] > n-1) return true; // 存在正环
q.push(v), inq[v] = true;
}
}
}
}return false;
}
void __ini__() {
memset(hd, 0, sizeof(hd));
for(int i=1; i<=n; ++i) dis[i] = -Inf;
memset(inq, false, sizeof(inq));
memset(cn, 0, sizeof(cn));
memset(can, false, sizeof(can));
cnte = 0;
return;
}
int main() {
while(scanf("%d", &n) != EOF && n != -1) {
__ini__();
for(int i=1; i<=n; ++i) {
int a, b, c; scanf("%d %d", &a, &b);
w[i] = a;
for(int j=0; j<b; ++j) {
scanf("%d", &c);
add_edge(i, c), can[i][c] = true;
}
}
if(!floyd()) puts("hopeless");
else {
if(spfa()) puts("winnable");
else puts("hopeless");
}
}
return 0;
}
题目:poj2983
题目算是比较直白的了我就不再复述。我们统一假设往北为正方向,这里我们要求解的未知量为距离。AB之间的距离要怎么表示才能构造出差分约束系统呢~我们可以自己假定一个源点,B到源点的距离为dis[b],A到源点的距离为dis[a],那么AB之间的距离就是dis[b]-dis[a]了,接着dis[]就是我们差分方程求解的未知数了。之后会看到超级源点会经常用在差分约束中来保证图是连通的。
- V:的情况很好列:dis[b] - dis[a] >= 1,移相后就是松弛操作dis[a] <= dis[b] - 1,这么移是因为个人喜欢利用最短路来求,如果用的是最长路来求则移相为>=的形式即可;
- P的情况是=,其实就是既>=又<=而已,同理列两个不等号方向统一的不等式;
- 最后再将超级源点与所有点连通保证图的连通性。
- 由于我们增加了超级源点,所以总的点数一共是n个,所以入队次数的判断终止是n次。
/*poj 2983*/
#include <cstdio>
#include <iostream>
#include <cstring>
#include <queue>
#define Inf 0x3f3f3f3f
#define ll long long
using namespace std;
const int N = 1e3+4, M = 1e5 + 4;
struct Edge {
int to, w, nxt;
Edge(int a=0, int b=0, int c=0): to(a), w(b), nxt(c) {}
}e[M*4];
int hd[N], cnte;
int dis[N], cn[N];
bool inq[N];
int n, m;
inline void add_edge(const int& f, const int& t, const int& w) {
++cnte; e[cnte] = Edge(t, w, hd[f]);
hd[f] = cnte; return;
}
inline int read() {
int ans = 0, tmp = 1;
char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-') tmp = -1;
else if(ch == 'P') return (int) ch;
else if(ch == 'V') return (int) ch;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
ans = (ans << 3) + (ans << 1) + ch-'0';
ch = getchar();
}return ans * tmp;
}
inline void __ini__() {
for(int i=0; i<=n; ++i) dis[i] = Inf;
memset(inq, false, sizeof(inq));
memset(hd, 0, sizeof(hd));
memset(cn, 0, sizeof(cn));
cnte = 0;
return;
}
bool spfa(const int& s) {
queue<int> q; while(q.size()) q.pop();
q.push(s);
inq[s] = true, cn[s] += 1, dis[s] = 0;
while(q.size()) {
int u = q.front(); q.pop();
inq[u] = false;
for(int i=hd[u]; i; i=e[i].nxt) {
int v = e[i].to;
if(dis[v] > dis[u] + e[i].w) {
dis[v] = dis[u] + e[i].w;
if(!inq[v]) {
if(++cn[v] > n) return false; // 一共n+1个点
inq[v] = true, q.push(v);
}
}
}
}return true;
}
int main() {
while(scanf("%d %d", &n, &m) != EOF) {
__ini__();
for(int i=0; i<m; ++i) {
char ch = (char) read();
if(ch == 'P') { // 精确
int a = read(), b = read(), c = read();
add_edge(b, a, -c),
add_edge(a, b, c);
}else {
int a = read(), b = read();
add_edge(b, a, -1);
}
}
for(int i=1; i<=n; ++i) add_edge(0, i, 0); // 超级源点!!只是为了图连通
if(spfa(0)) puts("Reliable");
else puts("Unreliable");
}
return 0;
}
这道题目看懂FAS, FAF等字符串的意思之后就比较明显了。如果以时间为所求变量的角度来看问题,则题目意思是给你四种不等式FAS, FAF, SAF, SAS要你求出所有时间的最小值,那么假设a的开始时间为Xa,时长为Wa,同理Xb,Wb,那么可以构造出四个不等式:
- FAS:Xa + Wa >= Xb 转换为查分约束系统 ==> Xa - Xb >= -Wa
- FAF:Xa + Wa >= Xb + Wb ==> Xa - Xb >= Wb - Wa
- SAF:Xa >= Xb + Wb ==> Xa - Xb >= Wb
- SAS:Xa >= Xb ==> Xa - Xb >= 0
由于求的是最小值,所以构造的是>=类型的不等式,将Xa,Xb看作到“源点”的距离dis,转换为求解最长路得到最小值。由于这里连的边不一定能够使得图能够连通,所以构造“超级源点”来将图连通。再来分析下spfa中的无解终止次数,由于我们自己增加了一个超级源点,所以总的点数为n+1,所以存在解最多遍历n次。
// 变量的命名重复啦啦, char s[] 和 int s[] ...
// 以后稍微小心点!
// hdu 1534
// solved
#include <iostream>
#include <cstdio>
#include <vector>
#include <queue>
#include <cstring>
#define Inf 0x3f3f3f3f
using namespace std;
const int N = 2000;
struct Edge {
int v, w;
Edge(int a=0, int b=0): v(a), w(b) {}
};
vector<Edge> g[N];
int n;
int w[N], d[N], cnt[N];
bool inq[N];
inline void __ini__() {
for(int i=0; i<=n; ++i)
vector<Edge>().swap(g[i]),
d[i] = -Inf;
memset(cnt, 0, sizeof(cnt));
memset(inq, false, sizeof(inq));
return;
}
inline void add_edge(int from, int to, int w) {
g[from].push_back(Edge(to, w));
return;
}
bool spfa() {
queue<int> q; while(q.size()) q.pop();
q.push(0);
inq[0] = true, d[0] = 0;
cnt[0] = 1;
while(q.size()) {
int u = q.front(); q.pop();
inq[u] = false;
for(size_t i=0; i<g[u].size(); ++i) {
int v = g[u][i].v,
w = g[u][i].w;
if(d[v] < d[u] + w) {
d[v] = d[u] + w;
if(!inq[v]) {
++cnt[v];
if(cnt[v] > n) return false; // n+1个点最多遍历n次
q.push(v), inq[v] = true;
}
}
}
}return true;
}
int main() {
int iter = 0;
while(~scanf("%d", &n) && n) {
__ini__();
for(int i=1; i<=n; ++i) {
scanf("%d", w+i);
}
char s[3];
while(~scanf("%s", s) && s[0] != '#') {
int a, b;
scanf("%d %d", &a, &b);
if(s[0] == 'F' && s[2] == 'S') // fas
add_edge(b, a, -w[a]);
else if(s[0] == 'F' && s[2] == 'F') // faf
add_edge(b, a, w[b]-w[a]);
else if(s[0] == 'S' && s[2] == 'F') // saf
add_edge(b, a, w[b]);
else add_edge(b, a, 0); // sas
}
for(int i=1; i<=n; ++i) {
add_edge(0, i, 0); // 所有起始>=0
}
iter += 1;
printf("Case %d:\n", iter);
if(spfa()) {
for(int i=1; i<=n; ++i) {
printf("%d %d\n", i, d[i]);
}
}else puts("impossible");
puts("");
}
return 0;
}
这道题算是比较难的了。题目大意:给出24个时间点所需的人手和N个应聘人的起始工作时间(工作时长为连续8小时),计算最少可以应聘多少人能够满足每个时间点的人手需求。
我们的已知条件:need[] 每个时间点所需要的人 app[]每个时间点应聘工作的人数。
目标:24小时里各时间点实际雇佣的人的总和。
我们设work[i]为在i时刻受聘的人,则
- work[i] + work[i-1] + ... + work[i-7] >= need[i],
- app[i] >= work[i],
- work[i] >=0 --请自行尝试转换为差分约束系统吧~
又出现了典型的求和式,利用前缀和转换为查分约束系统。这里提一下第一条式子,在i<8的情况下,dis[i] - dis[i+16] >= need[i] - dis[24],其中有三个变量,不满足差分约束的规则。但观察到每条式子都是dis[24]且dis[24]就是我们的答案,所以二分枚举dis[24]的值将其转化为常量就可以进行差分约束的求解。要注意枚举dis[24]的值将其看作常量实际上就相当于用一个变量(暂且叫ans吧)ans来替代dis[24],所以ans实际上是缺少差分系统中的约束的,所以要增加一条式子:dis[24] >= ans。
// cashier employment // 较难的差分 hdu 1529 // 关键--找到准约束的量,注意是对未知的量约束 // 同时涉及到了二分查找,查找符合的最小值(查找的有序区间0--n) // 由原式子 .. s[24](ans) - s[16] + s[i] >= r[i] // 可知ans的可行区域是向上开的, 所以二分查找是找到符合条件的就往下找,未找到则往上找 #include <iostream> #include <cstdio> #include <cstring> #include <queue> #define Inf 0x3f3f3f3f using namespace std; const int N = 1000+4; struct Edge { int to, w, nxt; Edge(int a=0, int b=0, int c=0): to(a), w(b), nxt(c) {} }e[N]; int hd[N], cnte, ans; int R[N], T[N], dis[N], cn[N]; bool inq[N]; inline void add_edge(const int& from, const int& to, const int& w) { ++cnte; e[cnte] = Edge(to, w, hd[from]); hd[from] = cnte; return; } inline void __ini__() { for(int i=0; i<=50; ++i) { dis[i] = -Inf, inq[i] = false; hd[i] = cn[i] = 0; }cnte = 0; return; } inline void new_g(const int& tmp) { // 重新建边 __ini__(); for(int i=1; i<=24; ++i) { add_edge(i, i-1, -T[i]), add_edge(i-1, i, 0); if(i >= 8) add_edge(i-8, i, R[i]); else add_edge(i+16, i, R[i]-tmp); }add_edge(0, 24, tmp); return; } bool spfa(const int& s) { // 找最长路 queue<int> q; while(q.size()) q.pop(); q.push(s), inq[s] = true; dis[s] = 0, cn[s] = 1; while(q.size()) { int u = q.front(); q.pop(), inq[u] = false; for(int i=hd[u]; i; i=e[i].nxt) { int v = e[i].to; if(dis[v] < dis[u] + e[i].w) { dis[v] = dis[u] + e[i].w; if(!inq[v]) { if(++cn[v] > 24) return false; // 一共25个点, 最多24次 inq[v] = true, q.push(v); } } } }return dis[24] >= 0; } bool bin_search(int l, int r) { // 二分枚举答案 ans = -1; while(l <= r) { int mid = (l+r) >> 1; new_g(mid); if(spfa(1)) { ans = mid; r = mid-1; // 查找最小 }else l = mid+1; }return ans >= 0; } int main() { int t; scanf("%d", &t); while(t--) { memset(T, 0, sizeof(T)); for(int i=1; i<=24; ++i) { scanf("%d", R+i); // i时刻至少需要的人 } int n; scanf("%d", &n); for(int i=0; i<n; ++i) { int tmp; scanf("%d", &tmp); T[tmp+1] += 1; // tmp时刻应聘的人, 注意时间全部+1 } if(bin_search(0, n)) printf("%d\n", ans); else puts("No Solution"); } return 0; }