差分约束学习笔记
一.差分约束系统
如果一个系统有 \(n\) 个变量 \(x_1,x_2,···,x_n\) 和 \(m\) 个约束条件(也是不等式)和\(m\) 个常量 \(w_1,w_2,···,w_m\)。每一个不等式形如以下格式 \(x_i - x_j \le w_k\)(\(1 \le i,j \le n\),\(1 \le k \le m\))。则称之为差分约束系统。
这个名字的由来是这样的:“差分”代表的是每一个约束条件里面都是做差的形式,“约束”代表的是每一个差都有一个最大值上界作为约束,“系统”字面意思。
有如下几个问题:
1.有没有解
2.求一组解
3.一组变量(\(a - b\))的极值
4.在额外条件下,变量和最值
...
容易发现,差分约束系统的解要么就无解,要么就有无数组解。
且这东西很容易识别,如果有一堆不等式每一个都形如 \(x_i - x_j \le w_k\) 这个格式,那么这个解决方法十有八九就是差分约束了。
这个东西的问题很清晰,接下来我们来看如何解决提出的这几个问题。
二.解的判断与求解
差分约束做了一个模型转化,将一个不等式组的问题转化为了一个图论问题。是不是很神奇?
在大学的计算机课程中,有一个东西叫做问题规约,即将一个问题的模型奇妙地转化为了另一个问题的模型。现在我告诉你,你只需要会求解图论中的最短路,你就可以做这个问题了。
我们将这个格式做一个变换:既然 \(x_i - x_j \le w_k\),则可以转换为 \(x_i \le x_j + w_k\)。
如果这时把 \(x\) 看成就是最短路中的 \(dis\) 数组,则 \(dis_i \le dis_j + w_k\)。
咦,这不就很像最短路中的松弛操作吗?
if (dis[v] > dis[u] + w)
dis[v] = dis[u] + w;
上面的两行代码相当于 dis[v] = min(dis[v], dis[u] + w)
这条语句。
于是我们得知,当进行一次松弛操作的时候,div[v]
一定 \(\le\) dis[u] + w
,即 \(x_i\) 一定满足 \(\le x_j + w_k\),恰好满足不等式!
如果将 \(w_k\) 看成 \(j \to i\) 的一条边,则恰好就可以完美吻合!
所以我们尝试将每一个不等式组 \(x_i - x_j \le w_k\) 变成 \(j \to i\) 的一条有向边,边权为 \(w_k\)。也就是将每一个约束条件都抽象成了一条边。
这时我们设任意的一个点为 \(S\),也就是起点,假设 \(x_S = 0\)。因为如果有解,就会有无数的解,就一定有 \(x_S = 0\) 为基础的这一组解。
则如果把该松弛的都松弛完了,也就是跑完了全图的从 \(S\) 开始的单源最短路,则如果 \(\forall 1 \le i \le n,x_i = dis_i\),对于任何的不等式都一定满足。
注意,这时候的图包含负权边(谁说 \(w_k\) 不能是负数了?),不能使用 dijkstra,需要使用 spfa 算法。
那么如何判断有没有解呢?
这个问题很简单,在上文的字里行间中就存在答案,这个字眼就是“跑完了单源最短路”。如果有某一个点的最短路不存在,则就是无解的。
什么?最短路不存在?你可以想到,不存在最短路,也就是无解,当且仅当图包含负环。
恰好 spfa 又可以判负环,于是在 spfa 求解最短路的过程中顺便判一下负环即可。
举一个例子:
这样的图中显然存在负环,不妨将其重新抽象成差分约束系统:
不妨将第一个式子和第二个式子联立起来,得到:\(x_1 - x_2 \le -4\),两边同时取相反数得到:\(x_2 - x_1 \ge 4\),但是这样又和第三个式子矛盾了,所以这个差分约束系统无解。
这样我们就同时解决了第一个和第二个问题。
三.特殊处理
看上去我们解决了第二个问题,但实际上第二个问题还没有结束呢!
\(2^{+}\):如果没有唯一的起点也就是 \(S\),也就是建出的图压根就不弱连通,怎么办呢?
这时候,我们可以弄一个超级源点,向所有点连一条边权为 \(0\) 的边。
但这个时候就有人会问了:那么所有点的最短路值不就全是 \(0\) 了吗?
这时候我们分情况。
如果所有的约束条件的 \(w\) 都是非负数
这时候所有点的最短路的值的确有可能会全部都是 \(0\)。
但是这个时候任意取两个数 \(x_i - x_j\) 都是 \(0\),而 \(0\) 显然 \(\le\) 全体非负数,一定成立。
如果有 \(w\) 是负数
这个时候最短路的值也有可能被不是 \(0\)。
因为有一个很巧合的点:\(0\) 显然 \(\ge\) 全体负数,所以也一定成立。
四.模板
P5960 【模板】差分约束
#include <bits/stdc++.h>
using namespace std;
int n, m;
const int N = 5010;
vector<pair<int, int> > v[N];
int dis[N], cnt[N];
bool vis[N];
bool spfa(int s) {//spfa算法判断负环
queue<int> q;
for (int i = 1; i <= n; i++)
dis[i] = 1e17;
dis[s] = 0, vis[s] = 1;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
vis[u] = 0;
for (auto [to, w] : v[u])
if (dis[to] > dis[u] + w) {
dis[to] = dis[u] + w;
cnt[to] = cnt[u] + 1;
if (cnt[to] >= n)
return 0;//找到负环
if (!vis[u])
q.push(to), vis[to] = 1;
}
}
return 1;
}
int main() {
cin >> n >> m;//这里设n+1号点为超级源点
n++;
for (int i = 1; i < n; i++)
v[n].push_back({i, 0});//连边
for (int i = 1; i <= m; i++) {
int x, y, w;
cin >> x >> y >> w;
v[y].push_back({x, w});//建图
}
if (!spfa(n))
cout << "NO\n";//发现负环就无解了
else {
for (int i = 1; i < n; i++)//输出每一个点的最短路作为一组解
cout << dis[i] << " ";
cout << endl;
}
return 0;
}
P1993 小 K 的农场
题面:有一堆式子和 \(n\) 的变量,这些式子形如:
第一种格式,\(x_a - x_b \ge c\)
第二种格式,\(x_a - x_b \le c\)
第三种格式,\(x_a = x_b\)
这下虽然不是普通的差分约束系统,但我们还是可以将其进行一些变换:
\(x_a - x_b \ge c\) 可以变成 \(x_b - x_a \le -c\),符合基本格式。
\(x_a - x_b \le c\) 本来就可以使用差分约束求解。
\(x_a = x_b\) 可以变成 \(x_a - x_b \le 0,x_b - x_a \le 0\),也符合基本格式。
这样就可以变成正宗的差分约束系统了。
int main() {
cin >> n >> m;
n++;
for (int i = 1; i < n; i++)
v[n].push_back({i, 0});
for (int i = 1; i <= m; i++) {
int op;
cin >> op;
if (op == 1) {
int x, y, w;
cin >> x >> y >> w;
v[x].push_back({y, -w});
} else if (op == 2) {
int x, y, w;
cin >> x >> y >> w;
v[y].push_back({x, w});
} else {
int x, y;
cin >> x >> y;
v[x].push_back({y, 0});
v[y].push_back({x, 0});//建图
}
}
if (!spfa(n))
cout << "No\n";
else
cout << "Yes\n";
return 0;
}
四.第三个问题
3.求 \(a-b\) 的最大或最小值。
最大值:以 \(b\) 为起点跑最短路,\(dis_a\) 就是答案。
最小值:以 \(b\) 为起点跑最长路,\(dis_a\) 就是答案。
五.第四个问题
4.额外条件下,变量和最大最小值
额外条件是因为差分约束可能有无穷组解,所以必须做出额外约束。
最大值:超级源点跑最短路,把所有的 \(dis_i\) 加在一起就是答案。