差分约束系统

10 篇文章 1 订阅

更多文章可以在本人的个人小站:https://kaiserwilheim.github.io 查看。
转载请注明出处。

我们经常会遇到一些问题,就是给出一堆未知量,再给出一堆类似“某一个未知量比另一个未知量最多(最少)大(小)多少”的问题,让我们从这一团繁杂的毛钱中拎出一根线头来,以求得这个毛线团的一组可行解。

比如说下面这个东西:

{ x 1 ≥ x 2 − 10 x 1 ≤ x 3 − 20 x 3 ≤ x 2 + 10 x 4 ≤ x 3 − 10 x 2 ≤ x 4 − 10 \begin{cases} x_1 \geq x_2 - 10 \\ x_1 \leq x_3 - 20 \\ x_3 \leq x_2 + 10 \\ x_4 \leq x_3 - 10 \\ x_2 \leq x_4 - 10 \end{cases} x1x210x1x320x3x2+10x4x310x2x410

我们就基本上无从下手。

这时候,我们就需要引入一个新的算法——差分约束

差分约束通过将不等式的问题转化为最短路(或最长路)问题来求解。

最短路?为什么?

回忆我们学习最短路的时候学到的知识。

假设对于两个点 a a a b b b,如果其间有一条边长为 w w w 的有向边 a → b a \to b ab,那么它们的 d i s dis dis 一定满足 d i s [ b ] ≤ d i s [ b ] + w dis[b] \leq dis[b] + w dis[b]dis[b]+w

我们如果将每一个未知量 x k x_k xk 与每一个点的 d i s [ i ] dis[i] dis[i] 联系起来的话,那么我们就可以得到形如 x b ≤ x a + w x_b \leq x_a + w xbxa+w 的一堆不等式。

而我们刚好需要这些不等式。

于是我们就可以将我们手中的不等式组转化为一堆边,并将其放到图里面,建成一个有向图。

最后得出的每一组合法的最短距离,都对应了一组不等式组的解。

我们首先把所有的式子转化为 x u ≤ x v + w x_u \leq x_v + w xuxv+w 的形式,再从每一个 v v v u u u 建一条边权为 w w w 的有向边。

这个有向图可以有环,毕竟环是不影响我们的求值环节的。
但是它不能有负环。

比如说下面这一组边:

{ x 2 ≤ x 1 + 10 x 3 ≤ x 2 + 10 x 1 ≤ x 3 − 30 \begin{cases} x_2 \leq x_1 + 10 \\ x_3 \leq x_2 + 10 \\ x_1 \leq x_3 - 30 \end{cases} x2x1+10x3x2+10x1x330

建到图上就是这个样子:

diffcon1.png

根据原始的不等式组,最终我们会得到 x 1 ≤ x 1 − 10 x_1 \leq x_1 - 10 x1x110 这样一个奇怪的式子,从而导致不等式组无解。

从图上看,这三条边构成了一个负环。

所以说,一个负环最终会导致出现一些奇怪的不等式,最终导致不等式组无解。
(当然,如果使用的是最长路的话就是正环)

那我们怎么判负环?(或者说正环)

SPFA!
(SPFA信徒狂喜)

实现

但是我们光靠这些边实际上是不能得出最终解的。
很多情况下,这张图根本不联通。
但是这样的图还是可以找到至少一组可行解的。

所以我们还需要建一个超级源点,向每一个点连一条长度为0的边。

应用

具体情况下,我们不仅有类似 x a ≤ x b + w x_a \leq x_b + w xaxb+w 这样的式子,还有其他的一些奇奇怪怪的约束条件。

下面列出了一些常见的约束条件和解决办法:

约束条件解决办法
x a ≤ w x_a \leq w xaw将源点到 a a a 的边权从0改到 w w w
x a ≥ w x_a \geq w xaw a a a 向源点连一条长为 − w -w w 的边。
x a = x b + w x_a = x_b + w xa=xb+w将其拆分为 x a ≤ x b + w x_a \leq x_b + w xaxb+w x b ≤ x a + ( − w ) x_b \leq x_a + (-w) xbxa+(w)
x a + x b ≤ w x_a + x_b \leq w xa+xbw差分约束会寄。请注意一下题目中有没有可以利用的其他特殊性质。

如果在一个不等式组的约束下(不等式组有解),想求出 x i − x j x_i − x_j xixj 的最大值呢?
首先一定有 x i − x j ≤ j x_i − x_j \leq j xixjj i i i 的"最短" 路 dis ⁡ ( j , i ) \operatorname{dis}(j,i) dis(j,i) 。因为我可以先走到 j j j ,然后走“ j j j i i i 的"最短路"”到 i i i
然后我们证明 x i − x j x_i − x_j xixj 可以取到这个值。
这相当于往不等式组中添加一个 x i − x j ≥ dis ⁡ ( j , i ) x_i − x_j \geq \operatorname{dis}(j,i) xixjdis(j,i) ,如果不等式组仍有解, x i − x j x_i − x_j xixj 就能取到 dis ⁡ ( j , i ) \operatorname{dis}(j,i) dis(j,i)
这也相当于在图中添加一条边,从 i i i j j j ,边权是 − dis ⁡ ( j , i ) −\operatorname{dis}(j,i) dis(j,i) 。这样加边一定不会出现负环,因为 dis ⁡ ( j , i ) \operatorname{dis}(j,i) dis(j,i) j j j i i i 的最短路,要有负环的话就有别的路径长度 < dis ⁡ ( j , i ) < \operatorname{dis}(j,i) <dis(j,i) 了。
如果要求 x i − x j x_i − x_j xixj 的最小值,就是求 x j − x i x_j − x_i xjxi 的最大值的相反数,即 − dis ⁡ ( i , j ) −\operatorname{dis}(i,j) dis(i,j)
x i − x j x_i − x_j xixj 的最小值” ≤ \leq x i − x j x_i − x_j xixj 的最大值”,对应了 − dis ⁡ ( i , j ) ≤ dis ⁡ ( j , i ) −\operatorname{dis}(i,j) \leq \operatorname{dis}(j,i) dis(i,j)dis(j,i) ,也就是 dis ⁡ ( i , j ) + dis ⁡ ( j , i ) ≥ 0 \operatorname{dis}(i,j) + \operatorname{dis}(j,i) \geq 0 dis(i,j)+dis(j,i)0 ,也就对应了图中没有包含 i , j i,j i,j 的负环。

代码

洛谷板子题

#include<bits/stdc++.h>
using namespace std;
const int N = 5010, M = 10010;
int e[M], ne[M], w[M], h[N], idx;
int tot[N], dis[N], vis[N];
int n, m;
void add(int a, int b, int c)
{
	e[++idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx;
}
bool spfa(int s)
{
	queue<int> q;
	memset(dis, 63, sizeof(dis));
	dis[s] = 0, vis[s] = 1;
	q.push(s);
	while(!q.empty())
	{
		int u = q.front();
		q.pop();
		vis[u] = 0;
		for(int i = h[u]; i; i = ne[i])
		{
			int v = e[i];
			if(dis[v] > dis[u] + w[i])
			{
				dis[v] = dis[u] + w[i];
				if(!vis[v])
				{
					vis[v] = 1, tot[v]++;
					if(tot[v] == n + 1)return false;
					q.push(v);
				}
			}
		}
	}
	return true;
}
int main()
{
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i++)
		add(0, i, 0);
	for(int i = 1; i <= m; i++)
	{
		int v, u, w;
		scanf("%d%d%d", &v, &u, &w);
		add(u, v, w);
	}
	if(!spfa(0))puts("NO");
	else
		for(int i = 1; i <= n; i++)
			printf("%d ", dis[i]);
	return 0;
}

例题

Luogu P1993 小 K 的农场

题目链接:https://www.luogu.com.cn/problem/P1993

接近板子题。

我们可以使用我们刚刚学到的技巧来完成这道题目。

#include<bits/stdc++.h>
using namespace std;
const int N = 5010, M = 20010;
int e[M], ne[M], w[M], h[N], idx;
int tot[N], dis[N], vis[N];
int n, m;
void add(int a, int b, int c)
{
	e[++idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx;
}
bool spfa(int s)
{
	queue<int> q;
	memset(dis, 63, sizeof(dis));
	dis[s] = 0, vis[s] = 1;
	q.push(s);
	while(!q.empty())
	{
		int u = q.front();
		q.pop();
		vis[u] = 0;
		for(int i = h[u]; i; i = ne[i])
		{
			int v = e[i];
			if(dis[v] > dis[u] + w[i])
			{
				dis[v] = dis[u] + w[i];
				if(!vis[v])
				{
					vis[v] = 1, tot[v]++;
					if(tot[v] == n + 1)return false;
					q.push(v);
				}
			}
		}
	}
	return true;
}
int main()
{
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i++)
		add(0, i, 0);
	for(int i = 1; i <= m; i++)
	{
		int op, v, u, w;
		scanf("%d%d%d", &op, &v, &u);
		if(op == 1)
		{
			scanf("%d", &w);
			add(v, u, -w);
		}
		else if(op == 2)
		{
			scanf("%d", &w);
			add(u, v, w);
		}
		else if(op == 3)
		{
			add(u, v, 0);
			add(v, u, 0);
		}
		else
		{
			puts("Youwike AK IOI!");
		}
	}
	if(!spfa(0))puts("No");
	else puts("Yes");
	return 0;
}

Luogu P6145 [USACO20FEB] Timeline G

题目链接:https://www.luogu.com.cn/problem/P6145

由于保证有解,而且我们得到的约束条件又都形如“第 b b b 次挤奶在第 a a a 次挤奶结束至少 x x x 天后进行”,所以我们得到的都是负权边。

我们可以进行 dp,也可以跑最短路。

#include<bits/stdc++.h>
using namespace std;
const int N = 100010, M = 400010;
const int INF = 0x3f3f3f3f;
int e[M], ne[M], w[M], h[N], idx;
int tot[N], dis[N], vis[N];
int n, m;
void add(int a, int b, int c)
{
	e[++idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx;
}
bool spfa(int s)
{
	queue<int> q;
	for(int i = 1; i <= n; i++)dis[i] = -INF;
	dis[s] = 0, vis[s] = 1;
	q.push(s);
	while(!q.empty())
	{
		int u = q.front();
		q.pop();
		vis[u] = 0;
		for(int i = h[u]; ~i; i = ne[i])
		{
			int v = e[i];
			if(dis[v] < dis[u] + w[i])
			{
				dis[v] = dis[u] + w[i];
				if(!vis[v])
				{
					vis[v] = 1, tot[v]++;
					if(tot[v] == n + 1)return false;
					q.push(v);
				}
			}
		}
	}
	return true;
}
int main()
{
	memset(h, -1, sizeof(h));
	int c;
	scanf("%d%d%d", &n, &m, &c);
	for(int i = 1; i <= n; i++)
	{
		int s;
		scanf("%d", &s);
		add(0, i, s);
	}
	for(int i = 1; i <= c; i++)
	{
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		add(u, v, w);
	}
	spfa(0);
	for(int i = 1; i <= n; i++)
		printf("%d\n", dis[i]);
	return 0;
}

Luogu P3275 [SCOI2011] 糖果

题目链接:https://www.luogu.com.cn/problem/P3275

我们遇到了新的约束条件:不带取等的不等式。

我们看一下题目的条件:分糖果

由于糖果是一块一块的,我们不能分给小朋友们半块糖果或 lim ⁡ m → 0 \lim_{m \to 0} limm0 块糖果,所以我们可以尝试着更改一下约束条件。
我们可以将 x a > x b x_a > x_b xa>xb 改为 x a ≥ x b + 1 x_a \geq x_b + 1 xaxb+1

这样就可以建图了。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 3000050;
const int INF = 0x3f3f3f3f;
int n, k;
int h[N], e[N], ne[N], w[N], idx;
int dis[N], tot[N];
bool vis[N];
void add(int a, int b, int c)
{
	e[++idx] = b;
	ne[idx] = h[a];
	h[a] = idx;
	w[idx] = c;
}
bool spfa(int s)
{
	queue<int> q;
	for(int i = 1; i <= n; i++)dis[i] = -INF;
	dis[s] = 0, vis[s] = 1;
	q.push(s);
	while(!q.empty())
	{
		int u = q.front();
		q.pop();
		vis[u] = 0;
		for(int i = h[u]; ~i; i = ne[i])
		{
			int v = e[i];
			if(dis[v] < dis[u] + w[i])
			{
				dis[v] = dis[u] + w[i];
				if(!vis[v])
				{
					vis[v] = 1, tot[v]++;
					if(tot[v] == n + 1)return false;
					q.push(v);
				}
			}
		}
	}
	return true;
}
int main()
{
	memset(h, -1, sizeof(h));
	scanf("%d%d", &n, &k);
	for(int i = 1; i <= k; i++)
	{
		int op, u, v;
		scanf("%d%d%d", &op, &u, &v);
		if(op == 1)
		{
			add(u, v, 0);
			add(v, u, 0);
		}
		else if(op == 2)
		{
			if(u == v)
			{
				puts("-1");
				return 0;
			}
			add(u, v, 1);
		}
		else if(op == 3)
		{
			add(v, u, 0);
		}
		else if(op == 4)
		{
			if(v == u)
			{
				puts("-1");
				return 0;
			}add(v, u, 1);
		}
		else if(op == 5)
		{
			add(u, v, 0);
		}
	}
	for(int i = n; i >= 1; i--)
		add(0, i, 1);
	if(!spfa(0))
	{
		puts("-1");
	}
	else
	{
		long long ans = 0;
		for(int i = 1; i <= n; i++)
			ans += dis[i];
		printf("%lld\n", ans);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值