【差分约束】学习笔记
差分约束
简介
差分约束常用于解决形如以下类型的问题:
- 给定若干个不等式 \(x_i-y_i\ge C_i\)(\(x_i,y_i\) 为未知数,\(C_i\) 为给定一常数),求其组成的不等式组的任意一组解(或判断其是否有解);
- 给定若干个不等式 \(x_i-y_i\le C_i\)(\(x_i,y_i\) 为未知数,\(C_i\) 为给定一常数),求其组成的不等式组的任意一组解(或判断其是否有解)。
例题:洛谷 P5960。
算法流程
其主要思想为将数学问题转化为图论问题。
我们以简介中的第二种类型的问题为例(即不等号均为 \(\le\))。
我们发现,如果直接运用数学方法对这个不等式组求解,在不等式数量较少时还略微可行,但是一旦不等式数量增加后,就变得十分吃力,因为我们需要对每个未知数通过联立不等式求其与其他未知数的大小关系,而这一操作需要进行多次遍历,且可能最后处理出多个大小关系,十分繁琐。
因此,我们应该考虑使用其他方法求解不等式组。一个自然的想法是,建立一个有向图,以每个未知数为节点,再用有向的边连接点与点来表示未知数与未知数之间的大小关系。我们可以将每个不等式变形得到新的不等式 \(x_i \le y_i+C_i\),我们不妨令每条权为 \(w\) 的边 \(u\to v\) 表示一个不等式:\(u + w \ge v\),即 \(v\le u + w\)。我们用上述方法建图后,考虑两点之间的距离的含义。
考虑一个简单的图:共三个未知数 \(v_1,v_2,v_3\),并且存在如下不等式关系:
建出图后如下图所示。我们注意 \(v_3\to v_1\) 的距离,为 \(-1+5=4\)。我们着重思考这一距离的含义。我们分段思考这个问题:

- 对于 \(v_3\to v_2\),边权为 \(-1\)。这表示 \(v_3-1\ge v_2\);
- 对于 \(v_2\to v_1\),边权为 \(5\)。这表示 \(v_2+5\ge v_1\),即 \(v_2\ge v_1 - 5\);
- 我们将上面两个式子联立起来,得到:\(v_3-1\ge v_2\ge v_1-5\),略去中间的 \(v_2\) 得到 \(v_3-1\ge v_1-5\),整理得到 \(v_1\le v_3+4\)。我们注意到,不等号的右侧正好出现了一个 \(4\),与 \(v_3\to v_1\) 的距离相等。再联系上面的联立过程,我们得出一个结论:在这张图上跑最短路,等同于联立经过的每一条边所表示的不等式,对于点对 \((u,v)\),其最终最短距离 \(d\) 即表示 \(v\le u+d\)。
在上面的结论上,我们考虑一个点 \(u\) 绕一圈回到自己的距离的含义,将这个点代入上面的含义,得到 \(u\le u + d\),为满足有解,其应该满足 \(\forall d, \text{s. t. } d \ge 0\)。为了满足对于所有的 \(d\) 都满足这一条件,因此我们应该考虑其最小的情况,即考虑最短路径的非负性,可以使用 Bellman-Ford 或 SPFA 算法检测是否存在负环。
现在算法的流程就已明了了,如下:
- 先以每个未知数为一个点,对每个不等式 \(x_i-y_i\le C_i\) 建一条权为 \(C_i\) 的边 \((y_i, x_i)\)。
- 在这张图上跑 Bellman-Ford 或 SPFA,检测是否存在负环:
- 若存在,则代表存在形如 \(x\le x-d\)(\(d>0\))的不等式,显然 \(\forall x\in \R\) 不满足此条件,故这个不等式组无解,输出 \(\texttt{NO}\);
- 若不存在,则代表有解。一组解即为从任意节点为起点出发跑最短路到每个节点的最短距离(答案不唯一)。
参考代码
void Bellman_Ford(int s) {
// Bellman-Ford 模板。
memset(dis, 0x3f, sizeof dis);
dis[s] = 0;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
if(dis[e[j].v] > dis[e[j].u] + e[j].w) {
// 在第 n 轮若还能继续松弛代表存在重复走边现象,即有负环,记录并退出。
if(i == n) {
fuhuan = 1;
return;
}
dis[e[j].v] = dis[e[j].u] + e[j].w;
}
}
}
}
void solve() {
cin >> n >> m;
for(int i = 1; i <= m; i++) {
// 等同于建边:y --(w)-> u
cin >> e[i].v >> e[i].u >> e[i].w;
}
for(int i = 1; i <= n; i++) {
// 由于可能图不连通,因此用一个超级联通源点 0 联通整张图。
e[++m] = {0, i, 0};
}
// Bellman-Ford 算法判断负环 & 求最短路
Bellman_Ford(0);
if(fuhuan) { // 有负环:无解,输出 NO。
cout << "NO" << endl;
} else { // 无负环:有解,其中一组通解是从超级源点 0 到所有点的最短距离。
for(int i = 1; i <= n; i++) {
cout << dis[i] << " ";
}
cout << endl;
}
}
变种问题
含有变种(1)、(2)的例题:洛谷 P1993。
(1)若两种类型的不等式混杂
则可以选择其中一种类型,将一个类型的全部不等式的两侧同时乘上 \(-1\),将 \(x_i,y_i\) 交换,\(C_i\) 乘上 \(-1\),这样就统一了所有类型。
(2)若含有等式
若含有形如 \(x_i=y_i+C_i\) 的等式,则可以表示成下面不等式组的唯一解:
再通过上面的变种(1)转变成:
随后就变成了常规类型了。
(3)若不等号不含相等情形
若含有形如 \(x_i-y_i<C_i\) 或 \(x_i-y_i>C_i\) 的不等式,下面以 \(<\) 为例。需要进行分类讨论:
- 若未知数保证是整数,则可以先整理成 \(x_i<y_i+C_i\),再表示成 \(x_i\le y_i+(C_i - 1)\);
- 若未知数不保证是整数,则可以表示成 \(x_i\le y_i+(C_i-eps)\)(此处 \(eps\) 表示极小的正分数,一般作 \(10^{-5}\) 或 \(10^{-8}\))。
(4)若符号为不为减号
- 若符号为加号 \(+\):形如 \(x_i+y_i\le C_i\) 或 \(x_i+y_i\ge C_i\),此时可以新开一个未知数 \(y'_i\) 表示 \(-y_i\),所以原不等式就转化成了 \(x_i-y'_i\le C_i\) 或 \(x_i-y'_i\ge C_i\),按照常规类型做即可;
- 若符号为乘号 \(\times\):形如 \(x_iy_i\le C_i\) 或 \(x_iy_i\ge C_i\),此时考虑将乘号转化成加号,再转化成减号。注意到对 \(x_iy_i\) 取对数后得到 \(\log{x_iy_i}=\log x_i+\log y_i\),因此只要将不等号两侧同时取对数就可以得到 \(\log x_i+\log y_i\le \log C_i\) 或 \(\log x_i+\log y_i\ge \log C_i\),转化成减号后做即可;
- 若符号为除号 \(\div\):同乘号,注意到 \(\log \left(\dfrac{x_i}{y_i}\right)=\log x_i-\log y_i\),因此可以将不等号两侧同时取对数得到 \(\log x_i-\log y_i\le \log C_i\) 或 \(\log x_i - \log y_i \ge \log C_i\),直接做即可。
核心思路即将其他运算转化成朴素的四则运算,再转化成最简单的减法做差分约束系统。
仍待补充。

浙公网安备 33010602011771号