計蒜客 – 蒜頭君的銀行卡 | 算盡天下系列第 10 期 | SPFA – 差分約束系統
上一期,長老向大家分享了一個跟 BFS 很像的、可以求解負環的單源最短路算法 SPFA,今天,讓我們來看一下 SPFA 在求解差分約束系統時的力量吧。
如果一個不等式組由 個變量和 個約束條件組成,且每個約束條件都是形如 的不等式,則稱其爲差分約束系統 (system of difference constraints)。
差分約束系統是求解一組變量的不等式組的算法,是最短路的一類經典應用。
爲什麼說它是最短路的一類經典應用呢?第一眼看過去難道不是一個數學問題嗎?實際上,我們在求解差分約束系統時,可以將其轉化爲圖論中單源最短路(或最長路)問題。
在求解最短路問題時,我們經常寫的一個不等式是 ,移項可得 ,這類似於不等式組中的 。所以我們可以理解成從頂點 到頂點 連一條權值爲 的邊,用最短路算法得到最短路的答案 ,也就求出了原不等式組的一個解。
因此我們可以將每個變量 作爲一個頂點,對於約束條件 ,連接一條邊權爲 的有向邊 <i, j>
。
連邊有兩種方式,第一種是連邊後求最長路,第二種是連邊後求最短路。
對於 ,若求最長路,則變形爲 ,從 到 連一條權值爲 的邊;若求最短路,則變形爲 ,從 到 連一條權值爲 的邊。
我們再增加一個超級源 , 連向其餘每個頂點,邊權均爲 。
對這個圖執行單源最短路算法,如果程序正常結束,那麼得到的最短路答案數組 d[i]
就是滿足條件的一組解;若圖中存在負環,則該不等式組無解。
考慮到差分約束系統的邊權可能爲負,我們套用前面介紹的 SPFA 算法可解決這個問題。
在學習了使用 SPFA 求解差分約束系統之後,讓我們來看一道例題,“計蒜客”的“蒜頭君的銀行卡”。
雖然蒜頭君並沒有多少錢,但是蒜頭君辦了很多張銀行卡,共有 張,以至於他自己都忘記了每張銀行卡里有多少錢了。
他只記得一些含糊的信息,這些信息主要以下列三種形式描述:
-
銀行卡 比銀行卡 至少多 元。
-
銀行卡 比銀行卡 至多多 元。
-
銀行卡 和銀行卡 裏的存款一樣多。
但是由於蒜頭君的記憶有些差,他想知道是否存在一種情況,使得銀行卡的存款情況和他記憶中的所有信息吻合。
輸入格式
第一行輸入兩個整數 和 ,分別表示銀行卡數目和蒜頭君記憶中的信息的數目。
接下來 行:
-
如果每行第一個數是 ,接下來有三個整數 ,表示銀行卡 比銀行卡 至少多 元。
-
如果每行第一個數是 ,接下來有三個整數 ,表示銀行卡 比銀行卡 至多多 元。
-
如果每行第一個數是 ,接下來有兩個整數 ,表示銀行卡 和 裏的存款一樣多。
3 3
3 1 2
1 1 3 1
2 2 3 2
輸出格式
如果存在某種情況與蒜頭君的記憶吻合,輸出 Yes
,否則輸出 No
。
Yes
這道題唯一的難點在於將不等式的表達形式轉換成圖中的邊,一旦正確地插入了邊,那麼直接套用 SPFA 的模板就可以了,不需要做任何其他的修改。
我們先來分析一下 和 中錢一樣多的情況。若 ,則等價於 $a \leqslant b $ 且 $ b \leqslant a$,那麼我們可以往圖中插入一條從 到 的權重爲 的邊,以及一條從 到 的權重爲 的邊。
if (type == 3) {
// (a == b) 等價於 (a <= b && b <= a)
insert(a, b, 0);
insert(b, a, 0);
}
若 比 至少多 元,那麼有 ,移項得到 ,即 ,對照上面的公式可以發現,應該從 到 連一條權值爲 的邊,求最短路;類似地,若 比 至多多 元,那麼 可得 ,從 到 連一條權值爲 的邊,求最短路。
if (type == 1) {
// a 比 b 至少多 c 元:(a - b >= c) => (b - a <= -c) => (a + (-c) >= b),從 a 到 b 連一條權值爲 -c 的邊,求最短路
insert(a, b, -c);
} else {
// a 比 b 至多多 c 元:(a - b <= c) => (a - c <= b) => (b + c >= a),從 b 到 a 連一條權值爲 c 的邊,求最短路
insert(b, a, c);
}
在正確插入所有的邊以後,加上一個超級源:
for (int i = 1; i <= n; i++) {
insert(0, i, 0); // 插入超級源,連向每一個點,權重爲 0
}
並且在 SPFA 的模板中修改 cnt[v] == n
時的情況:
if (cnt[v] == n + 1) {
// 因爲加入了超級源點,所以一共是 n + 1 個點
// 出現負環,不等式組無解
printf("No");
exit(0);
}
其餘的部分均直接套用 SPFA 模板,模板代碼可參見前一篇文章。