差分约束学习笔记
\(\texttt{0x00}\) 概念
差分约束系统是一种特殊的 \(n\) 元一次不等式组。
差分约束系统 是一种特殊的 \(n\) 元一次不等式组,它包含 \(n\) 个变量 \(x_1\sim x_n\) 以及 \(m\) 个约束条件,每个约束条件是由两个其中的变量做差构成的,形如
并且 \(c_k\) 是常数(可以是非负数,也可以是负数)。
我们要解决的问题是:求一组解
使得所有的约束条件得到满足,否则判断出无解。
\(\texttt{0x01}\) 求解过程
差分约束系统中的每个约束条件
都可以变形成
这与单源最短路中的三角形不等式
非常相似。
因此,我们可以把每个变量 \(x_i\) 看做图中的一个结点,对于每个约束条件 \(x_i-x_j\leq c_k\),从结点 \(j\) 向结点 \(i\) 连一条长度为 \(c_k\) 的有向边。
注意到,如果 \(\{a_1,a_2,\dots,a_n\}\) 是该差分约束系统的一组解,那么对于任意的常数 \(d\),\(\{a_1+d,a_2+d,\dots,a_n+d\}\) 显然也是该差分约束系统的一组解,因为这样做差后 \(d\) 刚好被消掉。
所以不妨先求一组负数解,即:
假设 \(\forall i,x_i \le 0\),这就意味着新建一个 \(0\) 号节点,令 \(x_0 = 0\),多了 \(n\) 个形如 \(x_i - x_0 \le 0\) 的约束条件,应该从 \(0\) 号节点向每个节点连一条长度为 \(0\) 的有向边。(可以看作一个超级源点)
令 \(dist[0] = 0\),以 \(0\) 为起点跑一遍单源最短路,(因为 \(c_k\) 可能为负数,相当于图中可能会有负权边,所以选用 spfa 算法)。
显然,\(x_i = dist[i]\) 就是差分约束系统的一组解。
那么无解情况呢?
推论:此差分约束系统无解 \(\Leftrightarrow\) 图中存在负环。
证明如下:
先证充分性:
若此差分约束系统无解,则一定是出现了 \(x_i < x_i\) 这样的关系,又根据原 \(m\) 个不等关系我们可以不断放缩,即
放缩可得:
又 \(\because x_i < x_i\)
\(\therefore c_{k_1} + c_{k_2} + \cdots + c_{c_{len}} < 0\)
对应到图中即:存在一个点数为 \(len + 1\) 的负环。
充分性得证。
再证必要性:
若图中存在负环,说明这个负环上的变量 \(x_p\sim x_q\) 中 \(x_p < x_{p + 1} <\cdots < x_q < x_p\),就得到了 \(x_p < x_p\),很显然矛盾了.
所以如果存在负环,那么给定的差分约束系统无解。
必要性得证。
\(\texttt{Q.E.D}\)
\(\texttt{0x02}\) 具体例题
P5960 【模板】差分约束
题目大意:
给定一个 \(n\) 元不等式,全是 \(x_i-x_j\leq c_k\) 的形式,若有解,求出其中一组可行解,否则输出 \(\texttt{NO}\) 表示无解。
对于每个关系 \(x_i-x_j\leq c_k\),直接在从 \(j\) 向 \(i\) 连一条长度为 \(c_k\) 的有向边。
然后建立一个超级源点,随便取一个基准值 \(\delta\) 从这个超级源点向每个点连一条长度为 \(\delta\) 的边,最后在这个超级源点跑一遍 spfa 求最短路就行了。
这里 \(\delta\) 取的是 \(0\)。
\(\texttt{Code:}\)
#include <queue>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 100010;
int n, m, C;
int h[N], e[N], w[N], ne[N], idx;
int dist[N], cnt[N];
bool vis[N];
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;
q.push(s);
memset(dist, 0x3f, sizeof dist);
dist[s] = 0;
vis[s] = true;
while(q.size()) {
int t = q.front();
q.pop();
vis[t] = false;
for(int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if(dist[j] > dist[t] + w[i]) {
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n + 1) return false;
dist[j] = dist[t] + w[i];
if(!vis[j]) {
vis[j] = true;
q.push(j);
}
}
}
}
return true;
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) add(0, i, 0); //建立超级源点并建边
for(int i = 1, a, b, c; i <= m; i++) {
scanf("%d%d%d", &a, &b, &c);
add(b, a, c); //注意a,b的顺序
}
if(!spfa(0)) puts("NO");
else for(int i = 1; i <= n; i++) printf("%d ", dist[i]);
return 0;
}
P6145 [USACO20FEB] Timeline G
题目大意:给定一个 \(n\) 元不等式,全是 \(x_i-x_j \ge c_k\) 的形式,并给定长度为 \(n\) 的序列 \(\{s\}\),\(\forall i \in [1,n]\),都有 \(x_i \ge S_i\) 。(满足一定有解)
求出所有 \(x_i\) 的最小值。
如果采用上一道题的建边方式。
把 \(x_i-x_j \ge c_k\) 变成 \(x_j-x_i \le -c_k\),然后从 \(i\) 向 \(j\) 连一条长度为 \(-c_k\) 的有向边,这没问题。
但是对于 \(\forall i \in [1,n]\),都有 \(x_i \ge S_i\) 这个条件,建立一个超级源点,令其为 \(0\) 号点,且 \(x_0 = 0\),则上式化成:
那么就应该从 \(i\) 向 \(0\) 连一条长度为 \(-S_i\) 的有向边,超级源点变成了超级汇点?所以这样做是不行的。
思来想去,考虑到这道题要求每个 \(x_i\) 的最小值,所以按道理根据这个不等式组,\(\forall i \in [1,n]\),都应该有 \(x_i \ge a_i\),而在求每个 \(x_i\) 时,可能有多个下界,为使它们全部满足,应该取它们中最大的那个,即:
其中 \(l\) 是下界个数。
这里就要用到一个结论,即:
求最小值则求最长路;
求最大值则求最短路。
所以这道题应该求最长路。
(所以讲了半天就是为了说明这个结论)
这样建图方式也应相应做出改变。
对于每个关系 \(x_i-x_j\ge c_k\),从 \(j\) 向 \(i\) 建一条长度为 \(c_k\) 的有向边。
对于 \(x_i \ge S_i\),化为 \(x_i - x_0 \ge S_i\),所以从 \(0\) 号点向 \(i\) 建一条长度为 \(S_i\) 的有向边。
最后在 \(0\) 号点用 spfa 跑一遍最长路即可。
\(\texttt{Code:}\)
#include <queue>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 200010;
int n, m, C;
int h[N], e[N], w[N], ne[N], idx;
int dist[N];
bool vis[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void spfa(int s) {
queue<int> q;
q.push(s);
memset(dist, -0x3f, sizeof dist);
dist[s] = 0;
vis[s] = true;
while(q.size()) {
int t = q.front();
q.pop();
vis[t] = false;
for(int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if(dist[j] < dist[t] + w[i]) {
dist[j] = dist[t] + w[i];
if(!vis[j]) {
vis[j] = true;
q.push(j);
}
}
}
}
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d%d", &n, &C, &m);
for(int i = 1, s; i <= n; i++) {
scanf("%d", &s);
add(0, i, s);
}
for(int i = 1, a, b, c; i <= m; i++) {
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
spfa(0);
for(int i = 1; i <= n; i++) printf("%d\n", dist[i]);
return 0;
}
从此我们可以总结出一个规律:
两个变量相减那边的建边是反过来的,即 \(a - b\) 则建边 \(b\to a\)
对于 \(x_i-x_j\ge c_k\) 这样的关系,思考跑最长路,执行
add(j, i, ck)
,或转化成 \(x_j-x_i\le -c_k\),执行add(i, j, -ck)
。
对于 \(x_i-x_j\le c_k\) 这样的关系,思考跑最短路,执行
add(j, i, ck)
,或转化成 \(x_j-x_i\ge -c_k\),执行add(i, j, -ck)
。
P1250 种树
这道题非常有意思,因为除了给出的数据需要建边,还有隐藏的建边关系。
注意这句话:
每个部分为一个单位尺寸大小并最多可种一棵树。
这其实隐藏了一个关系:\(\forall i \in (1,n],0\le x_i - x_{i - 1} \le 1\)。
同时,这里 \(x_i\) 的定义变成了前缀和,所以对于每个关系可以理解为 \(sum[b] - sum[a - 1] \ge c\)。
综上,再根据刚刚的建图方式,跑最长路。
\(\texttt{Code:}\)
#include <queue>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 30010, M = 100010;
int n, m, C;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
bool vis[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int ans;
void spfa(int s) {
queue<int> q;
q.push(s);
memset(dist, -0x3f, sizeof dist);
dist[s] = 0;
vis[s] = true;
while(q.size()) {
int t = q.front();
q.pop();
vis[t] = false;
for(int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if(dist[j] < dist[t] + w[i]) {
dist[j] = dist[t] + w[i];
if(!vis[j]) {
vis[j] = true;
q.push(j);
}
}
}
}
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
for(int i = 0; i <= n; i++) {
if(i) add(i - 1, i, 0);
if(i < n) add(i, i - 1, -1);
}
for(int i = 1, a, b, c; i <= m; i++) {
scanf("%d%d%d", &a, &b, &c);
add(a - 1, b, c);
}
spfa(0);
printf("%d", dist[n]);
return 0;
}
P3275 [SCOI2011] 糖果
这道题就更有意思了,(看上去像数据结构似的)。
首先发现要求最小值,所以跑最长路,并将所有关系都转化成大于等于。
一共有五种关系,分类讨论:
第一种操作:\(x_a = x_b\),根据 whk 上经常使用的方法可以转化为 \(x_a \le x_b\) 且 \(x_a \ge x_b\),所以在 \(a,b\) 间连一条长度为 \(0\) 的无向边。
第二种操作:\(x_a < x_b\),由于糖果数一定是整数,所以转化为 \(x_b - x_a \ge 1\),所以从 \(a\) 向 \(b\) 连一条长度为 \(1\) 的有向边。
第三种操作:\(x_a \ge x_b\),转化为 \(x_a - x_b\ge 0\),所以从 \(a\) 向 \(b\) 连一条长度为 \(0\) 的有向边。
第四种操作:\(x_a > x_b\),由于糖果数一定是整数,所以转化为 \(x_a - x_b \ge 1\),所以从 \(b\) 向 \(a\) 连一条长度为 \(1\) 的有向边。
第五种操作:\(x_a \le x_b\),转化为 \(x_a - x_b\le 0\),所以从 \(a\) 向 \(b\) 连一条长度为 \(0\) 的有向边。
考虑到每个小朋友都要拿到糖,所以建立一个超级源点 \(0\),向每个点连一条长度为 \(1\) 的边。
最后在 \(0\) 号点跑 spfa 求最长路,累加答案。
\(\texttt{Code:}\)
#include <queue>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 100010, M = 200010;
int n, m;
int h[N], e[M << 1], w[M << 1], ne[M << 1], idx;
int dist[N], cnt[N];
bool vis[N];
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) {
memset(dist, -0x3f, sizeof dist);
dist[s] = 0;
queue<int> q;
q.push(s);
vis[s] = true;
while(q.size()) {
int t = q.front();
q.pop();
vis[t] = false;
for(int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if(dist[j] < dist[t] + w[i]) {
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n + 1) return false;
dist[j] = dist[t] + w[i];
if(!vis[j]) {
vis[j] = true;
q.push(j);
}
}
}
}
return true;
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
int op, a, b;
for(int i = 1; i <= n; i++) add(0, i, 1);
for(int i = 1; i <= m; i++) {
scanf("%d%d%d", &op, &a, &b);
if(op == 1) add(a, b, 0), add(b, a, 0);
else if(op == 2) add(a, b, 1);
else if(op == 3) add(b, a, 0);
else if(op == 4) add(b, a, 1);
else add(a, b, 0);
}
if(!spfa(0)) puts("-1");
else {
int ans = 0;
for(int i = 0; i <= n; i++) ans += dist[i];
printf("%d\n", ans);
}
return 0;
}