【差分约束】总结

一、定义

现有 n n n 个变量 a 1 , a 2 , . . . , a n a_1, a_2, ... ,a_n a1,a2,...,an,给出 m m m 个不等式,第 i i i 个形如 a i − a j ≤ k a_i - a_j \leq k aiajk k k k 为常数, 1 ≤ i , j ≤ m 1 \leq i, j \leq m 1i,jm ),请构造出一组符合条件的数列 a a a

二、解法

要求构造 a a a,直接暴力不好搞,时间复杂度也很高,考虑转化问题。

d i s i dis_i disi 为超级原点到节点 i i i 的最短路径长,如果一个节点 u u u 可以通过一条权值为 w w w 的边到达节点 v v v 的话,根据最短路径的性质,则一定满足 d i s v ≤ d i s u + w dis_v \leq dis_u + w disvdisu+w。化简可得 d i s v − d i s u ≤ w dis_v - dis_u \leq w disvdisuw。与上述差分约束的不等式及其相似。所以当我们拿到一个不等式 a i − a j ≤ k a_i - a_j \leq k aiajk 时,可以在节点 j j j 和节点 i i i 间连接一条权值为 k k k 的有向边,再建立一个超级源点,求出到任意节点 i i i 的最短路径,便得到了一组答案。

当然,有时候题目中有隐藏的限制条件,这就因题而异了。

什么?你问怎么考虑无解的情况?还有无解的情况?!

是的,怎么考虑无解呢?

既然我们已经将问题转化成求解最短路了,所以只需要搞清楚最短路什么时候无解就行了。最短路无解的情况只有一种:图中存在 负环

因此,Dijkstra 死掉了。所以处理差分约束类问题时,我们一般选择 SPFA 算法。设节点 i i i 入队次数为 n u m i num_i numi,则当 n u m i ≥ n num_i \geq n numin 时,图中存在负环。

补充:常见的不等式转化方式如下:

a − b = k a - b = k ab=k,转化为 a − b ≤ k a - b \leq k abk a − b ≥ k a - b \geq k abk
a − b > k a - b > k ab>k,转化为 a − b ≥ k + 1 a - b \geq k + 1 abk+1
a − b < k a - b < k ab<k,转为为 a − b ≤ k − 1 a - b \leq k - 1 abk1

三、例题

1、差分约束系统

分析:

裸题,直接套用上述结论即可。

顺便挂个模板:
#include<bits/stdc++.h>
using namespace std;
#define SF scanf
#define PF printf
struct Edge {
	int to, next, w;
}edge[50005];
struct node {
	int dis, id;
};
int n, m, cnt, head[5005], num[5005], dis[5005];
bool vis[5005];
void add(int u, int v, int w) {
	edge[++cnt].to = v;
	edge[cnt].next = head[u];
	edge[cnt].w = w;
	head[u] = cnt;
}
void SPFA(int x) {
	for(int i = 1; i <= n; i++) dis[i] = 0x3f3f3f3f;
	dis[x] = 0;
	queue<node> q;
	q.push((node){0, x}), vis[x] = 1;
	while(!q.empty()) {
		node tmp = q.front();
		q.pop();
		vis[tmp.id] = 0;
		num[tmp.id]++;
		if(num[tmp.id] == n) {
			PF("NO");
			exit(0);
		}
		for(int i = head[tmp.id]; i; i = edge[i].next) {
			int to = edge[i].to;
			if(dis[to] > dis[tmp.id] + edge[i].w) {
				dis[to] = dis[tmp.id] + edge[i].w;
				if(!vis[to]) {
					q.push((node){dis[to], to});
					vis[to] = 1;
				}
			}
		}
	}
}
int main() {
	SF("%d%d", &n, &m);
	for(int i = 1; i <= n; i++) add(0, i, 0);
	for(int i = 1; i <= m; i++) {
		int u, v, w;
		SF("%d%d%d", &u, &v, &w);
		add(v, u, w);
	}
	SPFA(0);
	for(int i = 1; i <= n; i++) PF("%d ", dis[i]);
	return 0;
}

2、天平

分析:

显然输入给了我们很多不等式。
一共有 n n n 个砝码,设第 i i i 个砝码重 w i w_i wi 1 ≤ w i ≤ 3 1 \leq w_i \leq 3 1wi3 )克。
现在已经给出了两个砝码 A A A B B B。把它们放在了天平的一边,还需要另外两个砝码 C C C D D D,放在天平的另一边。
因为 4 ≤ n ≤ 50 4 \leq n \leq 50 4n50,范围很小,可以直接对 C C C D D D进行枚举。
要分别求出 w A + w B > w C + w D w_A + w_B > w_C + w_D wA+wB>wC+wD w A + w B = w C + w D w_A + w_B = w_C + w_D wA+wB=wC+wD w A + w B < w C + w D w_A + w_B < w_C + w_D wA+wB<wC+wD的方案数,设其分别为 c 1 c1 c1 c 2 c2 c2 c 3 c3 c3
关键在于怎样求出每一个 w i w_i wi,但这明显有点难,而且答案可能不唯一。
考虑对要求的三个不等式进行转化。
若要满足 w A + w B > w C + w D w_A + w_B > w_C + w_D wA+wB>wC+wD,即满足 w A − w C > w D − w B w_A - w_C > w_D - w_B wAwC>wDwB w A − w D > w C − w B w_A - w_D > w_C - w_B wAwD>wCwB 即可。
如果 min ⁡ { w A − w C } > max ⁡ { w D − w B } \min\{w_A - w_C\} > \max\{w_D - w_B\} min{wAwC}>max{wDwB} min ⁡ { w A − w D } > max ⁡ { w C − w B } \min\{w_A - w_D\} > \max\{w_C - w_B\} min{wAwD}>max{wCwB},则一定满足第一个不等式。累计 c 1 c1 c1
其余两个不等式转化方式同上。
至此,问题转化为:对于任意的 i i i j j j,设其满足条件的最小差值为 m i n n i , j minn_{i,j} minni,j,满足条件的最大差值为 m a x n i , j maxn_{i,j} maxni,j,求解出所有的 m i n n minn minn m a x n maxn maxn 即可。

Code:
#include<bits/stdc++.h>
using namespace std;
#define SF scanf
#define PF printf
int Min[55][55], Max[55][55], n;
void floyd() {
	for(int k = 1; k <= n; k++) {
		for(int i = 1; i <= n; i++) {
			if(i == k) continue;
			for(int j = 1; j <= n; j++) {
				if(j == k || i == j) continue;
				Min[i][j] = max(Min[i][j], Min[i][k] + Min[k][j]); //求最小跑最长
				Max[i][j] = min(Max[i][j], Max[i][k] + Max[k][j]); //求最大跑最短
			}
		}
	}
}
int main() {
	int A, B;
	SF("%d%d%d", &n, &A, &B);
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= n; j++) {
			char k;
			cin >> k;
			if(k == '+') Max[i][j] = 2, Min[i][j] = 1;
			else if(k == '=' || i == j) Max[i][j] = Min[i][j] = 0;
			else if(k == '-') Max[i][j] = -1, Min[i][j] = -2;
			else Max[i][j] = 2, Min[i][j] = -2;
		}
	}
	floyd(); //数据很小,保证有解,可以用 floyd
	int c1 = 0, c2 = 0, c3 = 0;
	for(int i = 1; i <= n; i++) {
		if(i == A || i == B) continue;
		for(int j = i + 1; j <= n; j++) {
			if(j == A || j == B) continue;
			if(Min[A][i] > Max[j][B] || Min[A][j] > Max[i][B]) c1++;
			if((Max[A][i] == Min[A][i] && Min[A][i] == Min[j][B] && Min[j][B] == Max[j][B]) || 
			   (Max[A][j] == Min[A][j] && Min[A][j] == Min[i][B] && Min[i][B] == Max[i][B])) c2++;
			if(Max[A][i] < Min[j][B] || Max[A][j] < Min[i][B]) c3++;
		}
	} 
	PF("%d %d %d", c1, c2, c3);
	return 0;
}

3、Intervals

分析:

设前 i i i 个数中选了 d i d_i di 个数。即要使每个 d i d_i di 尽可能小。
对于第 i i i 个条件: l i l_i li r i r_i ri 中至少选择 w i w_i wi 个数。
即给出了 d r i − d l i − 1 ≥ w i d_{r_i} - d_{l_i - 1} \geq w_i dridli1wi 这个限制条件。
转化成 d l i − 1 − d r i ≤ − w i d_{l_i - 1} - d_{r_i} \leq -w_i dli1driwi。就可以在 l i − 1 l_i - 1 li1 r i r_i ri 之间连接权值为 − w i -w_i wi 的有向边。
注意,这个题有隐藏条件!
你一定需要满足的条件是: 0 ≤ d i + 1 − d i ≤ 1 0 \leq d_{i + 1} - d_i \leq 1 0di+1di1
再根据这个不等式建边就行了。

Code:
#include<bits/stdc++.h>
using namespace std;
#define SF scanf
#define PF printf
struct Edge {
	int to, next, w;
}edge[200005];
int head[50005], cnt, dis[50005], n, s;
bool vis[50005];
void add(int u, int v, int w) {
	edge[++cnt].to = v;
	edge[cnt].next = head[u];
	edge[cnt].w = w;
	head[u] = cnt;
}
void SPFA(int x) {
	queue<int> q;
	q.push(x);
	for(int i = 1; i <= s; i++) dis[i] = 0x3f3f3f3f;
	vis[x] = 1, dis[x] = 0;
	while(!q.empty()) {
		int tmp = q.front();
		q.pop();
		vis[tmp] = 0;
		for(int i = head[tmp]; i; i = edge[i].next) {
			int to = edge[i].to;
			if(dis[to] > dis[tmp] + edge[i].w) {
				dis[to] = dis[tmp] + edge[i].w;
				if(!vis[to]) {
					vis[to] = 1;
					q.push(to); 
				}
			}
		}
	}
}
int main() {
	SF("%d", &n);
	for(int i = 1; i <= n; i++) {
		int u, v, w;
		SF("%d%d%d", &u, &v, &w);
		u++, v++; 
		s = max(s, v);
		add(v, u - 1, -w);
	}
	for(int i = 0; i < s; i++) add(i + 1, i, 0), add(i, i + 1, 1);
	SPFA(s);
	PF("%d", -dis[0]);
	return 0;
}

4、矩阵游戏

分析:

调整法是个不错的方法。
首先我们可以先令第 n n n 行和第 m m m 列的元素都为 0 ,这样我们就能够得到一组答案 a a a,虽然此答案可能不合法,但我们可以在其基础上进行调整。
我们设一个变量 c c c ,易证在任意一行 i i i 中,将第 j j j 个元素 a i , j a_{i,j} ai,j 变为 a i , j + ( − 1 ) j ∗ c a_{i,j} + (-1) ^ j * c ai,j+(1)jc,则该答案依旧合法。
再设一个变量 g g g ,易证在任意一列 r r r 中,将第 l l l 个元素 a l , r a_{l,r} al,r 变为 a l , r − ( − 1 ) l ∗ g a_{l,r} - (-1)^l * g al,r(1)lg,则该答案依旧合法。
这样对于任意一个元素 a i , j a_{i,j} ai,j,就将它变为了 a i , j + c − g a_{i,j} + c - g ai,j+cg a i , j − c + g a_{i,j} - c + g ai,jc+g
所以问题就转化成了:求解出各个行的 c c c 和各个列的 g g g 变量就可以了。
对其进行差分约束即可。

Code:
#include<bits/stdc++.h>
using namespace std;
#define SF scanf
#define PF printf
#define int long long
struct Edge {
	int to, next, w;
}edge[500005];
int a[305][305], b[305][305], head[605], cnt, n, m, dis[605], num[605];
bool vis[100005];
void add(int u, int v, int w) {
	edge[++cnt].to = v;
	edge[cnt].next = head[u];
	edge[cnt].w = w;
	head[u] = cnt;
}
bool SPFA(int x) {
	queue<int> q;
	q.push(x);
	for(int i = 1; i <= n + m; i++) dis[i] = 0x3f3f3f3f;
	dis[x] = 0, vis[x] = 1;
	while(!q.empty()) {
		int tmp = q.front();
		q.pop();
		vis[tmp] = 0;
		for(int i = head[tmp]; i; i = edge[i].next) {
			int to = edge[i].to;
			if(dis[to] > dis[tmp] + edge[i].w) {
				dis[to] = dis[tmp] + edge[i].w;
				if(++num[to] >= n + m) return false;
				if(!vis[to]) {
					vis[to] = 1;
					q.push(to);
				}
			}
		}
	}
	return true;
}
signed main() {
	freopen("matrix.in", "r", stdin);
	freopen("matrix.out", "w", stdout);
	int t;
	SF("%lld", &t);
	while(t--) {
		cnt = 0;
		memset(head, 0, sizeof(head));
		memset(vis, 0, sizeof(vis));
		memset(num, 0, sizeof(num));
		SF("%lld%lld", &n, &m);
		for(int i = 1; i < n; i++) {
			for(int j = 1; j < m; j++) SF("%lld", &b[i][j]);
		}
		for(int i = n - 1; i; i--) {
			for(int j = m - 1; j; j--) a[i][j] = b[i][j] - a[i + 1][j] - a[i][j + 1] - a[i + 1][j + 1];
		}
		for(int i = 1; i <= n; i++) {
			for(int j = 1; j <= m; j++) {
				if((i + j) & 1) {
					// 0 <= aij + ci - gj <= 1e6
					add(i, j + n, a[i][j]);
					add(j + n, i, 1e6 - a[i][j]);
				}
				else {
					// 0 <= aij - ci + gj <= 1e6
					add(j + n, i, a[i][j]);
					add(i, j + n, 1e6 - a[i][j]); 
				}
			}
		}
		bool flag = SPFA(1);
		if(!flag) {
			PF("NO\n");
			continue;
		}
		PF("YES\n");
		for(int i = 1; i <= n; i++) {
			for(int j = 1; j <= m; j++) {
				if((i + j) & 1) PF("%lld ", a[i][j] + dis[i] - dis[j + n]);
				else PF("%lld ", a[i][j] - dis[i] + dis[j + n]);
			}
			PF("\n");
		}
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值