差分约束的题目一般是:给一些约束条件(一些不等式),要求满足所有条件的一个解(最大或最小)。
对于xi - xj ≤ ki;这样的一个不等式,想到求最短路时的松弛技术(d[u]>d[v]+map[v][u])。
所以是否可以将差分约束转化为最短路的问题呢?
答案是肯定的。
xi,xj 对应于点u,v;ki对应于两点间的距离。
但是有几点要注意:
1、 关于松弛技术,是对于约束:d[u]≤d[v]+w[v,u] 的松弛,等号不能漏——具体证明参见《算导》;
所以当题目给出的约束条件为不等号>|<时,要记得转化(>k == ≥k+1 || <k == ≤ k-1)。
2、 (xi - xj) ≠ (xj - xi) 所以构造出来的图是有向图。
3、 全部转化为d[u]≤d[v]+w[v,u] 这种形式后,可能有负权边,所以不能用Dijkstra。
Bellman-ford, SPFA都可以。推荐SPFA。
5、 当图有负权环时,也就不存在满足约束条件的解。否则,SPFA的解就是要求的解。
6、最短路还是最长路: 所有的约束转化为"<="是求最短,转化为">="就是求最长。(不要忘记等号!)——具体理由见POJ1201的分析。
7、差分约束的很多题目都有隐含条件,如果不加这些隐含条件图可能不连通。当图可能不连通的时候,去找找有没有漏隐含条件。
解决图不连通的一个方法就是设立超级源点:
当然,首先要说的是:不是所有题目都要有超级源点。
比方POJ1364:由于我们要确定所有的不等式都要成立,即对于每个顶点都要求一次,这样很烦。而且最重要的是,这个题目没有给出其他的的隐含条件,也就是说单靠题目的条件构图,图可能是不连通的,所以也需要超级源点保持图的连通性。
一般超级源点有两种方法:
a. 超级源点——其实就是加一个x0点,使得这个点和所有点都相连,并且边权为0,然后从这个点进行SPFA,
这样其他每个顶点均从x0可达。
b.把所有的点一开始就入队,其实加入x0后,根据类似BFS的特性,第一步做的也就是把所有点都入队列。——不过这种方法就不需要x0了。
总之,差分约束原理和一些构图上的注意点大概就是以上这样了,主要是构图要正确。
例子:POJ 1364
首先要指出这个题目并不是很好,因为题目为了卡人,故意写的很绕。 - -
题目的意思是:
有一个序列S,里面有n个元素——S{a1, a2, ..., an}.;有m个子序列。
子序列的定义为:Si = {aSi, aSi+1, ..., aSi+ni}。———— 一开始我还以为是题目写错了,应该是ai,事实上这个a和S的ai没有半毛钱关系。
也就是说其实Si就是一个有ni个元素的连续整数序列,其中第一个整数位aSi,最后一个整数为aSi+ni。(由于所有Si的a都相同,其实就是[Si,Si+ni]。)
输入给出m个子序列的和一个对应ki的大小关系。
即aSi + aSi+1 + ... + aSi+ni < ki or aSi + aSi+1 + ... + aSi+ni > ki
设dist[Si+ni]表示从0加到Si+ni的总和:
对于小于号不等式转化为dist[Si+ni] - dist[Si-1] < ki
即 dist[Si+ni] - dist[Si-1] ≤ ki -1;
同理 dist[Si+ni] - dist[Si-1] > ki ; 即 dist[Si+ni] - dist[Si-1] ≥ ki+1;即dist[Si-1] - dist[Si+ni] ≤ -ki -1。
我是用超级源点的,题目中Si是从1开始的,所以Si-1是从0开始的,由于我选择0为源点,所以把每个点的标号都再加了1。
所以一共是1 到 n+1的n+1个点+超级源点 = n+2个点。
AC代码如下—— 之前邻接表写错了TLE,SPFA写错了WA。 T T。
1 #include<cstdio> 2 #include<cstring> 3 #include<queue> 4 using namespace std; 5 const int maxn = 111111; 6 const int INF = 1<<30; 7 bool visit[maxn]; 8 int dist[maxn]; 9 int vcnt[maxn];//记录入队次数 10 int head[maxn]; 11 int xxx; 12 struct node 13 { 14 int to,w,next; 15 }edge[maxn]; 16 void addedge(int from,int to,int w) 17 { 18 edge[xxx].to = to; 19 edge[xxx].w = w; 20 edge[xxx].next = head[from]; 21 head[from] = xxx++; 22 } 23 bool spfa(int start,int n) 24 { 25 queue<int> que; 26 memset(visit,false,sizeof visit); 27 memset(vcnt,0,sizeof vcnt); 28 visit[start] = true; 29 vcnt[start] = 1; 30 for(int i = 0;i<111;i++)dist[i] = INF; 31 dist[start] = 0; 32 que.push(start); 33 while(!que.empty()) 34 { 35 int top = que.front(); 36 que.pop(); 37 visit[top] = false; 38 int k = head[top]; 39 while(k!=-1) 40 { 41 int v = edge[k].to; 42 if( dist[v] > dist[top] + edge[k].w ) 43 { 44 dist[v] = dist[top] + edge[k].w ; 45 if(!visit[v]) 46 { 47 visit[v] = true; 48 que.push(v); 49 if(++vcnt[v]>n)return false; 50 } 51 } 52 k = edge[k].next; 53 } 54 } 55 return true; 56 } 57 int main() 58 { 59 #ifdef LOCALL 60 freopen("in","r",stdin); 61 //freopen("out","w",stdout); 62 #endif 63 char op[5];int n,m; 64 while(scanf("%d",&n),n) 65 { 66 scanf("%d",&m); 67 xxx = 0; 68 memset(head,-1,sizeof head); 69 for(int i = 0;i<m;i++) 70 { 71 int si,ni,ki; 72 scanf("%d%d%s%d",&si,&ni,op,&ki); 73 if(op[0] == 'l'){ 74 addedge(si,si+ni+1,ki-1); 75 } 76 else{ 77 addedge(si+ni+1,si,-ki-1); 78 } 79 } 80 for(int i = 1;i<n+2;i++) 81 { 82 addedge(0,i,0); 83 } 84 85 if(spfa(0,n+2))printf("lamentable kingdom\n"); 86 else printf("successful conspiracy\n"); 87 } 88 return 0; 89 }
POJ 3169
题目意思是有很多牛(n个)在一条线上,一些牛之间的距离要至少为ki,一些牛之间的距离最多为kj。问第一头牛和第n头牛的最大的距离。
“ it is possible that two or more cows can line up at exactly the same location”这句话提醒两头牛之间的最小距离是0,即(i+1,i)≥ 0。
第一头牛和第n头牛的最大的距离 : 对于 (xi,xj)≤ ki 来说就是求最短路。对于(xi,xj)≥ ki 来说就是求最长路。
AC代码如下:
1 #include<cstdio> 2 #include<cstring> 3 #include<queue> 4 using namespace std; 5 const int maxn = 111111; 6 const int INF = 0x7fffffff; 7 int ecnt; 8 bool visit[maxn]; 9 int dist[maxn]; 10 int vcnt[maxn];//记录入队次数 11 int head[maxn]; 12 struct node 13 { 14 int to,w,next; 15 }edge[maxn]; 16 void addedges(int from,int to,int w) 17 { 18 edge[ecnt].to = to; 19 edge[ecnt].w = w; 20 edge[ecnt].next = head[from]; 21 head[from] = ecnt++; 22 } 23 bool spfa(int start,int n) 24 { 25 queue<int> que; 26 memset(visit,false,sizeof visit); 27 memset(vcnt,0,sizeof vcnt); 28 for(int i = 0;i<=n;i++)dist[i] = INF; 29 dist[start] = 0; 30 visit[start] = true; 31 vcnt[start] = 1; 32 que.push(start); 33 while(!que.empty()) 34 { 35 int top = que.front(); 36 que.pop(); 37 visit[top] = false; 38 int k = head[top]; 39 while(k!=-1) 40 { 41 int v = edge[k].to; 42 if( dist[v] > dist[top] + edge[k].w ) 43 { 44 dist[v] = dist[top] + edge[k].w ; 45 if(!visit[v]) 46 { 47 visit[v] = true; 48 que.push(v); 49 if(++vcnt[v]>n)return false; 50 } 51 } 52 k = edge[k].next; 53 } 54 } 55 return true; 56 } 57 int main() 58 { 59 int n,ml,md; 60 while(scanf("%d%d%d",&n,&ml,&md)!=EOF) 61 { 62 ecnt = 0; 63 memset(head,-1,sizeof head); 64 for(int i = 0;i<ml;i++) 65 { 66 int a,b,c; 67 scanf("%d%d%d",&a,&b,&c); 68 // int d = a+b; 69 // a = a>b?a:b; b = d - a; 70 addedges(a,b,c); 71 } 72 for(int i = 0;i<md;i++) 73 { 74 int a,b,c; 75 scanf("%d%d%d",&a,&b,&c); 76 // int d = a+b; 77 // a = a>b?a:b; b = d - a; 78 addedges(b,a,-c); 79 } 80 for(int i = 1;i<=n;i++) 81 { 82 addedges(i+1,i,0); 83 } 84 if(spfa(1,n)) { 85 if(dist[n]<INF) 86 printf("%d\n",dist[n]); 87 else printf("-2\n");} 88 else { printf("-1\n"); 89 } 90 91 } 92 return 0; 93 }
POJ1201
很显然的差分约束,满足“1 <= ci <= bi - ai+1”, 隐藏区间可能是[a,a]的,也就是只有一个元素的。
设dist[i]是覆盖[0,i]区间的满足题意的子集中元素的最小个数。
1、ci <= bi - ai+1, 转化为dist[ai]-dist[bi+1]<=ci。 这里有个细节:题目说“0 <= ai <= bi <= 50000”,本来是转化为dist[ai-1]-dist[bi]<=ci,但是ai可能为0,ai-1就是-1了。这样程序交上去就会RE了。所以把2个点的标号都+1,变成dist[ai]-dist[bi+1]<=ci。
2、就是处理隐藏的区间的问题:1=>dist[i]-dist[i-1]>=0.
即:dist[i-1]-dist[i]<=0 和 dist[i] - dist[i-1] <=1。
然后用SPFA求最短路(要满足所有的<=的性质,只能是最小的——即最短路)。最后输出取反。
我看网上有很多人是求最长路的,其实是一样的:因为要满足所有的>=的性质,只能是最大的——即最长路。这时候构图把<=改为>=。然后spfa相应的初值等地方都要改一改。
不过这种方法不用判负环了。
(对于是求最短路,还是最长路就看变换为<=还是>=:如之前所说,d[a]-d[b]<=c对应最短路,d[a]-d[b]>=c对应最长路。)
写程序的时候,有几个细节要注意:
1、是上面提到的,ai可能为0的情况。
2、由于题目没给点的标号最大最小是多少,也就是说要自己记录一下,然后由于我们把每个点都+1了,计算的时候要注意不要忘了。
3、开数组的问题,用邻接表的话,记得“边”结构体数组开“点”个数的至少3倍——因为每个相邻点i ~ (i-1) 和 (i-1) ~i 记录了2条边,然后题目还会给n条边。一共最多可能3n条边。
—— WA+RE了几次,就是这些地方处理得不到位。
AC代码如下:c++和G++均过了。
1 #include<cstdio> 2 #include<cstring> 3 #include<queue> 4 using namespace std; 5 const int maxn = 55555; 6 const int INF = 1<<30; 7 bool visit[maxn]; 8 int dist[maxn]; 9 int vcnt[maxn];//记录入队次数 10 int head[maxn]; 11 int xxx; 12 struct node 13 { 14 int to,w,next; 15 }edge[maxn*3]; 16 void addedge(int from,int to,int w) 17 { 18 edge[xxx].to = to; 19 edge[xxx].w = w; 20 edge[xxx].next = head[from]; 21 head[from] = xxx++; 22 } 23 bool spfa(int start,int n) 24 { 25 queue<int> que; 26 memset(visit,false,sizeof visit); 27 memset(vcnt,0,sizeof vcnt); 28 visit[start] = true; 29 vcnt[start] = 1; 30 for(int i = start;i<=n;i++)dist[i] = INF; 31 dist[start] = 0; 32 que.push(start); 33 while(!que.empty()) 34 { 35 int top = que.front(); 36 que.pop(); 37 visit[top] = false; 38 int k = head[top]; 39 while(k!=-1) 40 { 41 int v = edge[k].to; 42 if( dist[v] > dist[top] + edge[k].w ) 43 { 44 dist[v] = dist[top] + edge[k].w ; 45 if(!visit[v]) 46 { 47 visit[v] = true; 48 que.push(v); 49 if(++vcnt[v]>(n-start+1))return false; 50 } 51 } 52 k = edge[k].next; 53 } 54 } 55 return true; 56 } 57 int main() 58 { 59 int n,nm; 60 while(scanf("%d",&n)!=EOF) 61 { 62 xxx = 0; 63 memset(head,-1,sizeof head); 64 nm = 0; 65 int start = 55555; 66 for(int i = 0;i<n;i++) 67 { 68 int a,b,c; 69 scanf("%d%d%d",&a,&b,&c); 70 addedge(a,b+1,-c); 71 nm = nm>(b+1)?nm:(b+1); 72 start = start<(a)?start:(a); 73 } 74 //printf("nm:%d\t,start:%d\n",nm,start); 75 for(int i = start;i<nm;i++) 76 { 77 addedge(i,i+1,0); 78 addedge(i+1,i,1); 79 } 80 if(spfa(start,nm))printf("%d\n",-dist[nm]); 81 else printf("0\n"); 82 } 83 return 0; 84 }
POJ2983
题目大意:格式P a b a,表示a在b点正北方刚好c的距离;V a b意思是a在b点正北方至少1个距离。
设a在b北方定义为dist[a]-dist[b],那么:
V a b 即 dist[a] - dist[b] >= 1;
P a b c 即 dist[a]-dist[b] = c; 即 c =<dist[a]-dist[b] <= c;
然后此题也要用到超级源点,因为要判断每个点作为起点有没有负环。题目点的标号从1开始,所以选择0作为超级源点。
用gets读入有空格的字串。
AC代码如下:写完忘记注释掉测试语句WA了一次。然后又是“边”数组一开始开111100——题目说的给出边的数目,忘了自己P的加了2条边,然后超级源点最多加了n条边。也就是说最多加了原来边数的2倍多——RE了一次。开两倍就AC了……
1 #include<cstdio> 2 #include<cstring> 3 #include<queue> 4 using namespace std; 5 const int maxn = 1111; 6 const int INF = 1<<30; 7 bool visit[maxn]; 8 int dist[maxn]; 9 int vcnt[maxn];//记录入队次数 10 int head[maxn]; 11 int xxx; 12 struct node 13 { 14 int to,w,next; 15 }edge[maxn*200]; 16 void addedge(int from,int to,int w) 17 { 18 edge[xxx].to = to; 19 edge[xxx].w = w; 20 edge[xxx].next = head[from]; 21 head[from] = xxx++; 22 } 23 bool spfa(int start,int n) 24 { 25 queue<int> que; 26 memset(visit,false,sizeof visit); 27 memset(vcnt,0,sizeof vcnt); 28 visit[start] = true; 29 vcnt[start] = 1; 30 for(int i = start;i<=n;i++)dist[i] = INF; 31 dist[start] = 0; 32 que.push(start); 33 while(!que.empty()) 34 { 35 int top = que.front(); 36 que.pop(); 37 visit[top] = false; 38 int k = head[top]; 39 while(k!=-1) 40 { 41 int v = edge[k].to; 42 if( dist[v] > dist[top] + edge[k].w ) 43 { 44 dist[v] = dist[top] + edge[k].w ; 45 if(!visit[v]) 46 { 47 visit[v] = true; 48 que.push(v); 49 if(++vcnt[v]>n)return false; 50 } 51 } 52 k = edge[k].next; 53 } 54 } 55 return true; 56 } 57 int main() 58 { 59 int n,m; 60 char line[22]; 61 int a[11]; 62 while(scanf("%d%d",&n,&m)!=EOF) 63 { 64 xxx = 0; 65 memset(head,-1,sizeof head); 66 char t = getchar(); 67 68 for(int i = 0;i<m;i++) 69 { 70 gets(line); 71 //printf("%s\n",line); 72 int len = strlen(line); 73 memset(a,0,sizeof a); 74 if(line[0] == 'P') 75 { 76 for(int i = 0,k = 2;i<3;i++) 77 { 78 while(line[k]!=' '&&line[k]!='\0') 79 { 80 a[i] = a[i]*10+(line[k]-'0'); 81 k++; 82 } 83 // printf("a[%d]:%d\t",i,a[i]); 84 k++; 85 } 86 //printf("P %d %d %d\n\n",a[0],a[1],a[2]); 87 addedge(a[0],a[1],a[2]); 88 addedge(a[1],a[0],-a[2]); 89 } 90 else 91 { 92 for(int i = 0,k = 2;i<2;i++) 93 { 94 while(line[k]!=' '&&line[k]!='\0') 95 { 96 a[i] = a[i]*10+(line[k]-'0'); 97 k++; 98 } 99 k++; 100 } 101 //printf("V %d %d\n",a[0],a[1]); 102 addedge(a[1],a[0],-1); 103 } 104 } 105 106 for(int i = 1;i<=n;i++) 107 { 108 addedge(0,i,0); 109 } 110 if(spfa(0,n+1))printf("Reliable\n"); 111 else printf("Unreliable\n"); 112 } 113 return 0; 114 }