\(\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;
}