【学习笔记】(1) 差分约束

1.算法介绍

差分约束系统 是一种特殊的 \(N\) 元一次不等式组,它包含 \(N\) 个变量 \(X_1 \sim X_N\) 以及 \(M\) 个约束条件,每个约束条件是由两个其中的变量做差构成的,形如 \(X_i - X_j \le c_k\),其中 \(1 \le i,j \le N, 1 \le k \le N\) 并且 \(c_k\) 是常数(可以是非负数,也可以是负数)。我们要解决的问题是:求一组解 ,\(X_1 = a_1,X_2 = a_2,\dots,x_N = a_N\) 使得所有的约束条件得到满足,否则判断出无解。

差分约束系统中的每个约束条件 \(X_i - X_j \le c_k\) 都可以变形成 \(x_i \le X_j + c_k\),这与单源最短路中的三角形不等式 \(dis[y] \le dis[x] + z\) 非常相似。因此,我们可以把每个变量 \(X_i\) 看做图中的一个结点 \(i\),对于每个约束条件 \(X_i - X_j \le c_k\),从结点 \(j\) 向结点 \(i\) 连一条长度为 \(c_k\) 的有向边,然后再从超级源点 0 向每个点连长度为 0 的边防止图不连通(或者一开始令所有 \(dis_i = 0\) 并将所有点入队),跑最短路,每个点的最短路长度就是一组解。。

注意到,如果 \(\{a_1,a_2,\dots,a_N \}\) 是该差分约束系统的一组解,那么对于任意的常数 \(\Delta\)\(\{a_1+\Delta,a_2+\Delta,\dots,a_N+\Delta \}\) 显然也是该差分约束系统的一组解,因为这样做差后 \(\Delta\) 刚好被消掉。

因为一般这个 \(c_k\) 有负值(全是非负的话所有数相等不就行了嘛),所以用 Bellman-Ford 或 SPFA 求解最短路。显然,若出现负环则无解。最坏时间复杂度 \(O(nm)\)

模板题 P5960 【模板】差分约束算法

#include<bits/stdc++.h>
#define N 5005
using namespace std;
int read(){
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
	while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
	return x * f;
}
int n, m, tot;
int to[N], Head[N], Next[N], edge[N];
bool vis[N];
int dis[N], in[N];
queue<int> q;
void add(int u, int v, int w){
	to[++tot] = v, Next[tot] = Head[u] ,Head[u] = tot, edge[tot] = w;
}
int main(){
	n = read(), m = read();
	for(int i = 1; i <= m; ++i){
		int v = read(), u = read(), w = read();
		add(u, v, w);
	}
	for(int i = 1; i <= n; ++i) q.push(i), vis[i] = 1, ++in[i]; //这里也可以建一个超级源点
	while(!q.empty()){
		int x = q.front(); q.pop(); vis[x] = 0;
		for(int i = Head[x]; i; i = Next[i]){
			int y = to[i];
			if(dis[y] > dis[x] + edge[i]){
				dis[y] = dis[x] + edge[i];
				if(!vis[y]) ++in[y], vis[y] = 1, q.push(y);
				if(in[y] > n) return printf("NO\n"), 0;
			}
		}
	}
	for(int i = 1; i <= n; ++i) printf("%d ",dis[i]); printf("\n");
	return 0;
}

2.解的字典序极值

一般而言差分约束系统的解没有 “字典序” 这个概念,因为我们只对变量之间的差值进行约束,而变量本身的值可以随着某个变量取值的固定而固定,所以解的字典序可以趋于无穷大或无穷小。

字典序的极值建立于变量有界的基础上,假如 \(X_i \le 0\) ,要求出整个差分约束系统的字典序最大。其实 \(X_i \le 0\) 可以看成 \(X_i \le X_0 + 0\),所以我们建立虚点 0,将其初始 \(dis\) 赋为 0,并向其它所有变量连权值为 0 的边(这等价于一开始令所有点入队且 \(dis_i=0\))。

首先明确一点,对于一条从 $ u → v$ 的边权为 \(w(u,v)\) 的边,它的含义为限制 $X_u+w(u,v)\ge X_v $。

那么通过 SPFA 求得的一组解,恰为字典序最大解。

证明:

考虑 0 到每个节点的最短路树。对于树上每条边均满足 \(X_i+w(i,j)=X_j\)。若 \(X_i+w(i,j)>X_j\),那么整张图还可以继续松弛,若 \(X_i+w(i,j)<X_j\),说明 \(j\) 的最短路不是由 \(i\) 继承而来,因此 \((i,j)\) 必然不会作为树边出现。

这说明树上的 \(X_i+w(i,j)\ge X_j\) 的限制已经取满,取到了等号(\(X_j\) 不能再大了,再大就破坏了限制),每个变量都取到了其理论上界,自然就得到了字典序最大解。

证毕

对于字典序最小解,我们限制 \(X_i\ge 0\)。那么其实我们可以将限制转为 \(X_i \ge X_0 + 0\), 0 向所有点连一个边 \(w(0,i)\) \(X_j \ge X_i - c_k\),即与之前求最大时见相反的边,权值也相反,(可以自己画一下图好好理解一下),用 SPFA 跑最长路,得到的解就是字典序最小。

3.例题

3.1 P5590 赛车游戏

好题。

我们可以转化一下思路,与其设置边权使路径长度相等,不如设置路径长度去拟合边权的限制。

\(dis_i\)
\(1→i\) 的最短路,只需保证对于所有边 \(w(u,v)\) 有 $ w(u,v) = dis_v = dis_u $ 即可使任意一条简单路径长相等。于是 $ 1 \le dis_v - dis_u \leq 9$ ,转化为 \(dis_u+9 \ge dis_v\) 与 $ dis_v - 1\ge dis_u$,差分约束求解即可。注意不在 \(1→n\) 的任意一条路径上的边没有用,这些边不应计入限制,随便标权值,浅浅来个 rand就行了。

#include<bits/stdc++.h>
#define N 1005
#define M 4005
using namespace std;
int read(){
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
	while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
	return x * f;
}
int n, m, tot;
int Head[N], to[M], Next[M], edge[M];
int dis[N], in[N];
bool vis[N], VIS[2][N], flag[N];
vector<int> E[2][N];
struct edge{
	int u, v;
}a[M];
void add(int u, int v, int w){
	to[++tot] = v, Next[tot] = Head[u], Head[u] = tot, edge[tot] = w;
}
void dfs(int x, int k){
	VIS[k][x] = 1;
	for(auto y : E[k][x]){
		if(VIS[k][y]) continue;
		dfs(y, k);
	}
}
bool spfa(int s){
	memset(dis, 0x3f, sizeof(dis));
	queue<int> q; q.push(s);
	vis[s] = 1, dis[s] = 0, ++in[s];
	while(!q.empty()){
		int x = q.front(); q.pop(), vis[x] = 0;
		for(int i = Head[x]; i; i = Next[i]){
			int y = to[i];
			if(dis[y] > dis[x] + edge[i]){
				dis[y] = dis[x] + edge[i];
				if(!vis[y]) ++in[y], vis[y] = 1, q.push(y);
				if(in[y] > n) return true;
			}
		}
	}
	return false;
}
int main(){
	srand(time(0));
	n = read(), m = read();
	for(int i = 1; i <= m; ++i){
		a[i].u = read(), a[i].v = read();
		E[0][a[i].u].push_back(a[i].v), E[1][a[i].v].push_back(a[i].u);
	}
	dfs(1, 0), dfs(n, 1);
	if(!VIS[0][n]) return printf("-1\n"), 0;
	for(int i = 1; i <= n; ++i) flag[i] = (VIS[0][i] & VIS[1][i]);
	for(int i = 1; i <= m; ++i){
		if(flag[a[i].u] && flag[a[i].v]) add(a[i].u, a[i].v, 9), add(a[i].v, a[i].u, -1);
	}
	if(spfa(1)) return printf("-1\n"), 0;
	printf("%d %d\n", n, m);
	for(int i = 1; i <= m; ++i){
		if(flag[a[i].u] && flag[a[i].v]) printf("%d %d %d\n", a[i].u, a[i].v, dis[a[i].v] - dis[a[i].u]);
		else printf("%d %d %d\n", a[i].u, a[i].v, rand() % 9 + 1);
	}
	return 0;
}

3.2 CF241E Flights

基本同上题,把边权限制改一下在进行差分约束就行了。

#include<bits/stdc++.h>
#define N 1005
#define M 10005
using namespace std;
int read(){
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
	while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
	return x * f;
}
int n, m, tot;
int Head[N], to[M], Next[M], edge[M];
int dis[N], in[N];
bool vis[N], VIS[2][N], flag[N];
vector<int> E[2][N];
struct edge{
	int u, v;
}a[M];
void add(int u, int v, int w){
	to[++tot] = v, Next[tot] = Head[u], Head[u] = tot, edge[tot] = w;
}
void dfs(int x, int k){
	VIS[k][x] = 1;
	for(auto y : E[k][x]){
		if(VIS[k][y]) continue;
		dfs(y, k);
	}
}
bool spfa(int s){
	memset(dis, 0x3f, sizeof(dis));
	queue<int> q; q.push(s);
	vis[s] = 1, dis[s] = 0, ++in[s];
	while(!q.empty()){
		int x = q.front(); q.pop(), vis[x] = 0;
		for(int i = Head[x]; i; i = Next[i]){
			int y = to[i];
			if(dis[y] > dis[x] + edge[i]){
				dis[y] = dis[x] + edge[i];
				if(!vis[y]) ++in[y], vis[y] = 1, q.push(y);
				if(in[y] > n) return true;
			}
		}
	}
	return false;
}
int main(){
	srand(time(0));
	n = read(), m = read();
	for(int i = 1; i <= m; ++i){
		a[i].u = read(), a[i].v = read();
		E[0][a[i].u].push_back(a[i].v), E[1][a[i].v].push_back(a[i].u);
	}
	dfs(1, 0), dfs(n, 1);
	for(int i = 1; i <= n; ++i) flag[i] = (VIS[0][i] & VIS[1][i]);
	for(int i = 1; i <= m; ++i){
		if(flag[a[i].u] && flag[a[i].v]) add(a[i].u, a[i].v, 2), add(a[i].v, a[i].u, -1);
	}
	if(spfa(1)) return printf("No\n"), 0;
	printf("Yes\n");
	for(int i = 1; i <= m; ++i){
		if(flag[a[i].u] && flag[a[i].v]) printf("%d\n", dis[a[i].v] - dis[a[i].u]);
		else printf("%d\n", rand() % 2 + 1);
	}
	return 0;
}

3.3 P3275 [SCOI2011]糖果

这题可以将题中五个关系改成差分约束的限制。

  • 如果 \(X=1\), 表示第 \(A\) 个小朋友分到的糖果必须和第 \(B\) 个小朋友分到的糖果一样多,改为 \(dis_A \ge dis_B + 0\)\(dis_B \ge dis_A + 0\)
  • 如果 \(X=2\), 表示第 \(A\) 个小朋友分到的糖果必须少于第 \(B\) 个小朋友分到的糖果,改为 \(dis_B \ge dis_A + 1\)
  • 如果 \(X=3\), 表示第 \(A\) 个小朋友分到的糖果必须不少于第 \(B\) 个小朋友分到的糖果,改为 \(dis_A \ge dis_B\)
  • 如果 \(X=4\), 表示第 \(A\) 个小朋友分到的糖果必须多于第 \(B\) 个小朋友分到的糖果,改为 \(dis_A \ge dis_B + 1\)
  • 如果 \(X=5\), 表示第 \(A\) 个小朋友分到的糖果必须不多于第 \(B\) 个小朋友分到的糖果,改为 $dis_B \ge dis_A $;

至于为什么这么改,因为题中要求的是满足限制的最少糖果数且 \(dis_i \ge 1\),所以相当于是求字典序最小,可以本篇上文 2.解的字典序极值。另外,还要建立一个超级源点 0 向 所有点连边权为 1 的边,来满足 \(dis_i \ge 1\) 的限制。

还有这题由于数据范围比较大,我们最好不使用 SPFA ,以防被卡。我们可以使用 Tarjan 缩点来判正环,有正环就无解,具体地,对于在同一强连通分量的点来说,如果他们之间的边为 1 ,那么肯定有正环,因为边权要么为 1 ,要么为 0 。 之后由于 Tarjan 缩点后 成了 DAG 图,我们可以用拓扑来求解答案,对于同一强连通分量的点他们得到的糖果数显然是相同的。

#include<bits/stdc++.h>
#define M 300005
#define N 100005
#define ll long long
using namespace std;
int read(){
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
	while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
	return x * f;
}
int n, m, tot, top, t, cnt;
ll ans = 0;
int to[M], Next[M], Head[N], edge[M], fr[M];
int f[N], in[N];
bool vis[N];
int dfn[N], low[N], s[N], col[N], sz[N];
void add(int u, int v, int w){
	to[++tot] = v, fr[tot] = u, Next[tot] = Head[u], Head[u] = tot, edge[tot] = w;
}
void tarjan(int x){
	dfn[x] = low[x] = ++t, s[++top] = x, vis[x] = 1;
	for(int i = Head[x]; i; i = Next[i]){
		int y = to[i]; 
		if(!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]);
		else if(vis[y]) low[x] = min(low[x], dfn[y]);
 	} 
 	if(dfn[x] == low[x]){
 		int k = -1; ++cnt;
 		while(k != x){
 			k = s[top--], ++sz[cnt], vis[k] = 0, col[k] = cnt;
		}
	}
}
int main(){
//	freopen("1.in","r",stdin);
	n = read(), m = read();
	for(int i = 1; i <= m; ++i){
		int opt = read(), x = read(), y = read();
		if(opt == 1) add(x, y, 0), add(y, x, 0);
		else if(opt == 2) add(x, y, 1);
		else if(opt == 3) add(y, x, 0);
		else if(opt == 4) add(y, x, 1);
		else add(x, y, 0);
	}
	for(int i = 1; i <= n; ++i) add(0, i, 1);
	for(int i = 0; i <= n; ++i) if(!dfn[i]) tarjan(i);
	m = tot; memset(Head, 0, sizeof(Head)); tot = 0;
	for(int i = 1; i <= m; ++i){
		int x = col[fr[i]], y = col[to[i]];
		if(x == y && edge[i]) return printf("-1\n"), 0;
		if(x != y) add(x, y, edge[i]), ++in[y];
	}
	queue<int> q;
	for(int i = 1; i <= cnt; ++i) if(!in[i]) q.push(i);
	while(!q.empty()){
		int x = q.front(); q.pop(); 
		for(int i = Head[x]; i; i = Next[i]){
			int y = to[i]; --in[y];
			f[y] = max(f[y], f[x] + edge[i]);
			if(!in[y]) q.push(y);
		}
	}
	for(int i = 1; i <= cnt; ++i) ans += 1ll * f[i] * sz[i];
	printf("%lld\n", ans);
	return 0;
}

3.4 P3530 [POI2012]FES-Festival

将关系转为差分约束的限制,发现是稠密图,考虑使用 Floyd , \(n^3\) 剪枝一下还是能过的。

还是要 Tarjan 缩点,然后考虑无解的情况:

  1. 如果是负环,那么肯定无解,判定条件 : \(dis[i][i] < 0\)

  2. 零环显然可以

  3. 如果对于正环,可以发现如果全是 1 的话 才是无解,因为在建 \(m1\) 的边时,有正反边权,那么 全是 1 的正环 其实也是全是 -1 的负环,如果环中没有为 0 的边,其实如果任意方向权值和不为0,一定无解,因为反一下就有正负环,而有 0 边 的话,可以限制方向,如果与大多数的 边权为1 的边相同的话,那么就有解,因为无法反转了,被 0 这条单向边限定死了,否则无解。

其实说了那么多,其实就只要判负环就可以了。

那最后的答案呢?其实就是各个强连通分量内的最长路之和 + 强连通分量数之和。为什么可以这样呢?因为各个强连通分量如果有连接的话,必然是 0 边,因为 1 和 -1 是配套,方向相反,那如果不是 0 边,其实两个强连通分量是在同一个强连通分量内的,不符合。那这样两个强连通分量之间就可以无限拉长,就可以分开来考虑。按照差分约束算法,从一个点 \(x_a\) 到另一个点 \(x_b\) 的最短路代表 \(x_b - x_a\) 的最大值,并考虑到边权只有 \(\{ 1,-1,0\}\) 三种,可以得到结论: 一个强连通分量内的最多取值个数等于强连通分量两两之间最短路的最大值

证明:

强连通分量两两之间最短路的最大值就代表 \(x_b - x_a\) 最大的值,即值域跨度最大,那在路径上,值要么 +1, 要么 -1 ,要么 0 (不变) ,那么显然能将 \([x_a,x_b]\) 之间的数都取一遍,那值域可以再大一些吗?显然不行,不然违反了两两之间最短路的最大值的定义。因为将 \(a\) 逆方向移动一下都会使值变大, 将 \(b\) 顺方向移动一下会使值变小,都会使得值域变小,显然不优,好好想一下吧,有些情况是无解,反正这样一定是最优的。

画一下图可以发现,这样的 \(a\), \(b\) 一定出现在边的临界点,而且最短路其实就是最长路,因为\(a \sim b\)路径值只可能有一种值。

证毕。

后记:一直以为自己懂了,直到写题解时才发现自己可能没有真的理解,这道题还有好多值得探究的问题,有些我可能讲不明白,讲起来还可能有点繁琐,没有图不是很直观,而且我也不想画了,那就这样吧,至少我知道了(bushi)

#include<bits/stdc++.h>
#define N 605
#define M 200005 
#define INF 0x3f3f3f3f
using namespace std;
int read(){
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
	while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
	return x * f;
}
int n, m1, m2, tot, t, top, scc, sum;
int Head[N], Next[M], to[M], edge[M];
int low[N], dfn[N], s[N], col[N], sz[N];
bool vis[N];
int dis[N][N], ans[N];
void add(int u, int v, int w){
	to[++tot] = v, Next[tot] = Head[u], Head[u] = tot, edge[tot] = w; 
}
void tarjan(int x){
	dfn[x] = low[x] = ++t, vis[x] = 1, s[++top] = x;
	for(int i = Head[x]; i; i = Next[i]){
		int y = to[i];
		if(!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]);
		else if(vis[y]) low[x] = min(low[x], dfn[y]);
	}
	if(low[x] == dfn[x]){
		int k = -1; ++scc;
		while(k != x){
			k = s[top--], vis[k] = 0, ++sz[scc], col[k] = scc;
		} 
	}
}
int main(){
	n = read(), m1 = read(), m2 = read();
	memset(dis, 0x3f, sizeof(dis));
	for(int i = 1; i <= m1; ++i){
		int u = read(), v = read();
		dis[u][v] = 1, dis[v][u] = -1;
		add(u, v, 1), add(v, u, -1);
	}
	for(int i = 1; i <= m2; ++i){
		int u = read(), v = read();
		if(dis[v][u] == INF) dis[v][u] = 0;
		add(v, u, 0);
	} 
	for(int i = 1; i <= n; ++i){
		dis[i][i] = 0;
		if(!dfn[i]) tarjan(i);
	}
	for(int k = 1; k <= n; ++k){
		for(int i = 1; i <= n; ++i){
			if(col[i] != col[k] || dis[i][k] == INF) continue;
			for(int j = 1; j <= n; ++j){
				if(col[i] != col[j]) continue;
				dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
			}
		}
	}
	for(int i = 1; i <= n; ++i){
		if(dis[i][i] < 0) return printf("NIE\n"), 0;
		for(int j = 1; j <= n; ++j){
			if(col[i] == col[j]) ans[col[i]] = max(ans[col[i]], dis[i][j] + 1);
		}
	}
	for(int i = 1; i <= scc; ++i) sum += ans[i];
	printf("%d\n", sum);
	return 0;
}

参考资料: OI WiKi初级图论

posted @ 2023-05-20 18:36  Aurora-JC  阅读(138)  评论(0编辑  收藏  举报