更多文章可以在本人的个人小站: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} ⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧x1≥x2−10x1≤x3−20x3≤x2+10x4≤x3−10x2≤x4−10
我们就基本上无从下手。
这时候,我们就需要引入一个新的算法——差分约束。
差分约束通过将不等式的问题转化为最短路(或最长路)问题来求解。
最短路?为什么?
回忆我们学习最短路的时候学到的知识。
假设对于两个点 a a a 和 b b b,如果其间有一条边长为 w w w 的有向边 a → b a \to b a→b,那么它们的 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 xb≤xa+w 的一堆不等式。
而我们刚好需要这些不等式。
于是我们就可以将我们手中的不等式组转化为一堆边,并将其放到图里面,建成一个有向图。
最后得出的每一组合法的最短距离,都对应了一组不等式组的解。
我们首先把所有的式子转化为 x u ≤ x v + w x_u \leq x_v + w xu≤xv+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} ⎩⎪⎨⎪⎧x2≤x1+10x3≤x2+10x1≤x3−30
建到图上就是这个样子:
根据原始的不等式组,最终我们会得到 x 1 ≤ x 1 − 10 x_1 \leq x_1 - 10 x1≤x1−10 这样一个奇怪的式子,从而导致不等式组无解。
从图上看,这三条边构成了一个负环。
所以说,一个负环最终会导致出现一些奇怪的不等式,最终导致不等式组无解。
(当然,如果使用的是最长路的话就是正环)
那我们怎么判负环?(或者说正环)
SPFA!
(SPFA信徒狂喜)
实现
但是我们光靠这些边实际上是不能得出最终解的。
很多情况下,这张图根本不联通。
但是这样的图还是可以找到至少一组可行解的。
所以我们还需要建一个超级源点,向每一个点连一条长度为0的边。
应用
具体情况下,我们不仅有类似 x a ≤ x b + w x_a \leq x_b + w xa≤xb+w 这样的式子,还有其他的一些奇奇怪怪的约束条件。
下面列出了一些常见的约束条件和解决办法:
约束条件 | 解决办法 |
---|---|
x a ≤ w x_a \leq w xa≤w | 将源点到 a a a 的边权从0改到 w w w。 |
x a ≥ w x_a \geq w xa≥w | 从 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 xa≤xb+w 和 x b ≤ x a + ( − w ) x_b \leq x_a + (-w) xb≤xa+(−w)。 |
x a + x b ≤ w x_a + x_b \leq w xa+xb≤w | 差分约束会寄。请注意一下题目中有没有可以利用的其他特殊性质。 |
如果在一个不等式组的约束下(不等式组有解),想求出
x
i
−
x
j
x_i − x_j
xi−xj 的最大值呢?
首先一定有
x
i
−
x
j
≤
j
x_i − x_j \leq j
xi−xj≤j 到
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
xi−xj 可以取到这个值。
这相当于往不等式组中添加一个
x
i
−
x
j
≥
dis
(
j
,
i
)
x_i − x_j \geq \operatorname{dis}(j,i)
xi−xj≥dis(j,i) ,如果不等式组仍有解,
x
i
−
x
j
x_i − x_j
xi−xj 就能取到
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
xi−xj 的最小值,就是求
x
j
−
x
i
x_j − x_i
xj−xi 的最大值的相反数,即
−
dis
(
i
,
j
)
−\operatorname{dis}(i,j)
−dis(i,j)。
“
x
i
−
x
j
x_i − x_j
xi−xj 的最小值”
≤
\leq
≤“
x
i
−
x
j
x_i − x_j
xi−xj 的最大值”,对应了
−
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}
limm→0 块糖果,所以我们可以尝试着更改一下约束条件。
我们可以将
x
a
>
x
b
x_a > x_b
xa>xb 改为
x
a
≥
x
b
+
1
x_a \geq x_b + 1
xa≥xb+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;
}