先約定:記邊數 ,點數 ,點集 ,邊集 。
這篇 blog 旨在掛博主這隻鴿子。
網絡:有向圖 ,對每條邊 有容量 ,有唯一源點 和唯一匯點 。其中若 ,則 。
對一個網絡 ,定義映射 ,滿足:
- 容量約束:
- 反對稱:
- 流守恆:
稱這個映射爲網絡 的流,該網絡是一個流網絡。
注意看好這裏的定義,特別是流守恆,不要弄錯網絡流研究的問題。在網絡的一個可行流中,對於除源匯外的任意一個點,都應該滿足 Σ流入 = Σ流出 !絕對不是 流出 ≤ Σ流入 之類的!
表示 到 的流量。 的時候、稱 這條邊飽和。
是網絡的流。
點的高度:點到匯點的不加權最短路徑長度;
點的層次:點到源點的不加權最短路徑長度。
事實上因爲習慣的原因,高度和層次這兩個概念的定義經常會比較模糊(
殘量(剩餘流量函數):
給一個網絡 , 是 的流。
——定義上面兩個流的和 ,滿足 ,可以證得它的容量約束、反對稱、流守恆三個性質,於是流的和也是流。
增廣路:在剩餘網絡裏面的一條 到 的路徑。
這個增廣路和二分圖裏面的增廣路是不一樣的;當然也有相似的地方。
在流網絡中,從源點流入無限的流量,然後求在匯點那裏最多可以流出多少流量。
最常用的算法應該是 Dinic ,寫 ISAP 也挺好的。
最大流-最小割定理:最小割 = 最大流。
——所以可以暴力枚舉最小割,根據最大流-最小割定理求最大流的值。 。
容易想到每次找增廣路,更新流量,直到殘量網絡內源點和匯點在兩個連通塊內。
這樣就能得到最優解嗎?
簡單起見,暫且認爲增廣一律用 DFS 。
模擬幾次就可以發現,假如我們現在有多條增廣路,我們顯然會隨機選一條增廣;然而這個時候,我們可能會走一條屆不到最大流的錯誤的路。那好像就有可能得不到最優解了。有沒有辦法改進呢?
應該意識到的是,我們要堅持從源點出發到匯點的原則。就是說我們還是要找增廣路。然後呢,之前那條劣的增廣路:我們假設現在它中間有一些邊的一部分容量不應該被浪費。
實際上甚至可以設得更簡單一些:我們可以考慮它中間有一條邊整條都不應該走,這個要推廣回去是很簡單的。
那麼,這條路徑除了這條邊的部分還是要走的:這些部分顯然還在最大流方案裏面。所以我們要找到最大流方案,就相當於要給這條路徑刪掉不該走的部分、然後補上另外的路。
怎麼做能夠方便實現?現在上面那條路徑變成了三部分,左部分、要刪的、右部分。首先我們會從源點引一段路接上右邊的部分。然後從左邊的部分引一條路到匯點。會做這種事情的前提顯然是走現在的兩條路比走原來的路徑更優。
比較大小可以化差,於是不難想到用負權邊來代替“刪除走錯的邊”這一操作。
現在我們還是從源點開始,先引一條路到右部分的左端:這時候我們不要去匯點,而是應該走那條負權邊,然後剛好連到左部分的右端,接着引一條路到匯點。這樣就用一條路完成了刪補補三件事情。
“刪除走錯的邊”的根本目的還是得到更優的解,增廣也是要得到更優的解,注意到這個,於是會產生把前者統一到後者的想法;把前者統一到後者,那麼我們要用後者的方法解決前者,就是走增廣路;
注意到會損失一部分貢獻然後加上另一部分貢獻,聯想到負這個概念,所以想到了負權路,放到實現裏面也發現是反向邊。
這條負權邊應該是要反着走的,也就是反向邊;所以建圖的時候就得多建一倍的邊了。現在建圖的時候就得先把一條邊跟它的反向邊配對。之後我們叫正、反向邊就不是建圖時候的意思了,而是相對的。
比如說,一開始的正向邊的反向邊是對應的一開始的反向邊,然後一開始的反向邊的反向邊是對應的一開始的正向邊。
爲了配合各種定義,走一條有向邊的時候要給這條邊的容量減掉流過去的量,給對應反向邊的容量加上同樣的量。
這樣的話,我們一直增廣,一定能找到最優解。現在也不用擔心最大流會被阻塞了。同時也不會出現無限循環的事情。因爲每次增廣都會對答案有貢獻。
於是可以得到 Ford-Fulkerson 方法 :每次找增廣路,沿增廣路更新流量。
稱採用 Ford-Fulkerson 方法的一類求最大流的算法爲增廣路算法。
注意:這個方法如果用 DFS 實現的話,只保證有解和能找到解,效率嘛……雖然每次增廣都會產生貢獻,但是產生的貢獻可能會每次都很少。
比如:+1 +1 +1 …
記最大流爲 ,最多找 次增廣路,增廣一次 ,則複雜度 。
前面 DFS 會炸是因爲 DFS 的增廣是盲目的。但是 BFS 不一樣。
可以每次用 的 BFS 找增廣路增廣,這樣和用 DFS 增廣的不同之處在於 BFS 找到的增廣路的長度(不帶權)是單調不遞減的,並且在這種情況下可證得最多增廣次,於是複雜度 。
引入了層次圖的概念。
構建層次圖, 次 BFS 增廣直到源匯不連通,然後重新構建層次圖增廣,重複上述步驟直到層次圖中的源匯不連通就找到最大流了。複雜度 。
它跑網絡流和二分圖都比較快。很常見。
Dinic 算法用一次 DFS 代替 MPLA 的多次 BFS 來進行增廣。則 Dinic’s algorithm 的複雜度是 ;
特別地,對 unit network , Dinic’s algorithm 的複雜度是 。
由此可見,在二分圖上(最大匹配)跑 Dinic’s algorithm 的時候,複雜度是。
Dinic 算法可以加一個 cur (當前弧)優化:在某一輪構建出的層次圖裏,對於從一個點引出去的多條邊,假如裏面有幾條邊(在這一輪裏)被走過了,我們就不會再走那幾條邊。
當前弧優化的前提是我們 DFS 的時候,會後退當且僅當沒有路可以走了。不過這個同時也是爲了保證複雜度必須採取的移動策略,所以這是廢話(
另外我們還可以再加一個優化,就是 BFS 構建層次圖沒有必要構建完,只要源匯連通就可以了。它的可行性在於,我們不用實際地構建層次網絡,只需要記錄一下層次,所以源匯連通的時候,我們已經遍歷完深度更低的所有點了。顯然我們需要的那一部分邊都出來了。
PS. 建層次圖可以從源點開始 BFS 也可以從匯點開始,這個看個人喜好?
可以用 LCT 優化成 。
首先、邊在 LCT 裏面變成點。然後用 LCT 實現以下操作:
- 插入:加邊(增廣)、直到源點和匯點都在同一棵樹裏;
- 查詢:找到增廣路上殘量最小的邊;
- 修改:更新增廣路上邊的殘量;
- 刪除:刪掉殘量爲零的邊。
分析一下複雜度?
- 一般流網絡 的證明:
根據層次和層次網絡的定義,最多也就是構建 次層次圖;每次 BFS 構建層次圖是 的。
現在考慮增廣的時間複雜度。本來我們 BFS 增廣,每次至少讓一條邊消失, ;現在改用 DFS 增廣,要期望它的複雜度有所降低,就要從另外的角度進行分析。嘗試把目光放到整個層次圖上來分析。這同時也是因爲 DFS 的過程是盲目的、難以預測的;我們並沒有一個所謂更優的選擇路徑的策略。
於是要將 DFS 對複雜度的貢獻分開來計算,嘗試拆成修改的複雜度和移動的複雜度。
修改 是怎麼樣的?我們考慮整張層次圖,按照上面的結論,一張層次圖增廣 次,每次增廣修改的次數是 的。這個結論對於 BFS 同樣適用,這也正如我們所預料的: BFS 最有可能出現冗餘步驟的地方正是它的搜索過程。
分析 DFS 的 移動 。之前提過不少次了,增廣的次數是 的;但是我們一次增廣,移動的步數現在就不一定是 的了,畢竟不可能一下子就找到增廣路。所以我們要對那些找不到增廣路的步驟進行分析。“找不到增廣路”,實際上講的是不能到達,或者說是走到的點沒有出度了。這個時候我們顯然要回溯:同時,這個點已經是沒用的了,可以刪除。很顯然,我們刪除一個點同時也意味着我們後退了一步。把後退的代價拆出來,後退次數是 的,同時對答案有貢獻的“前進”次數前面分析過了是 的。需要注意的是,這一部分的複雜度分析基於層次圖的設計和正確的移動策略(就是說不要隨便後退,一定要等到無路可走了再退)。
可以說層次圖是一個很巧妙的設計。它將一開始 EK 中 BFS 的優勢轉移到層次圖上來,爲 DFS 增廣提供了可能性。
Dinic 的常數一般比較優秀(當然想要讓它跑滿很輕鬆【參考】)。
在一些數據下,可以在 DFS 增廣的時候不給反向邊加流量,等到增廣完畢再加上去(就是等到下一次增廣的時候再退這一次的流),可能會快一點?【引用】
#include <cstdio>
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <cctype>
#include <queue>
#include <algorithm>
using namespace std;
const long long inf = 2147483647147483647;
int n, m, S, T, tot = 1;
int head[105], nxt[20005], to[20005], dep[105], cur[105];
long long cap[20005];
#define add_edge(a, b, c) nxt[++tot] = head[a], head[a] = tot, to[tot] = b, cap[tot] = c
int q;
queue<int> Q;
bool pre() {
while (!Q.empty()) Q.pop();
for (register int i = 1; i <= n; ++i) dep[i] = -1;
dep[S] = 0;
Q.push(S);
while (!Q.empty()) {
q = Q.front();
Q.pop();
if (q == T) {
return 1;
}
for (register int i = head[q]; i; i = nxt[i]) {
if (cap[i] > 0 && dep[to[i]] < 0) {
dep[to[i]] = dep[q] + 1;
Q.push(to[i]);
}
}
}
return 0;
}
void update(const int &i, const long long &c) {
cap[i] -= c;
cap[i ^ 1] += c;
}
long long preflow;
long long aug(int x, long long flow) {
if (x == T) {
return flow;
}
for (register int i = cur[x]; i; i = nxt[i]) {
cur[x] = i;
if (dep[x] + 1 == dep[to[i]] && cap[i] > 0) {
preflow = aug(to[i], min(flow, cap[i]));
if (preflow == 0) {
continue;
}
return update(i, preflow), preflow;
}
}
return 0;
}
int main() {
long long c;
scanf("%d%d%d%d", &n, &m, &S, &T);
for (register int a, b, i = 1; i <= m; ++i) {
scanf("%d%d%lld", &a, &b, &c);
add_edge(a, b, c);
add_edge(b, a, 0);
}
long long maxflow = 0, temp = 0;
while (pre()) {
for (register int i = 1; i <= n; ++i) cur[i] = head[i];
while (temp = aug(S, inf)) maxflow += temp;
}
printf("%lld", maxflow);
return 0;
}
- 理論複雜度依舊是 ,不過常數(在大多場合下)很小,而且代碼短。【參考】
ISAP 可以算作是改進的 Dinic 算法(雖然 SAP 是指 EK ……)。Dinic 每次都要重建層次圖,但是層次圖的改動實際上並不大——於是想到嘗試在 DFS 的時候順便修改層次圖。
比如:現在在某個點上,當前層次圖中這個點已經不能再拓展了,這時候我們就要修改這個點的層次以尋找繼續拓展的可能性。
現在需要找一條路往後走,那麼應該是要把當前點的層次修改到可以接上後面至少一個點的程度。同時因爲我們要保證走的是最短路,很顯然是要接到匯點最短的那一條。
所以現在我們記錄的不應該是點的層次了,而應該是點的高度(不過爲了方便一般還是叫做層次,就當作是把 作爲源點的層次之類的?)
所以我們每次先從 開始 BFS 初始化層次圖,然後 DFS 找增廣路順便更新層次圖。
另外, ISAP 一般還會加上一個 GAP 優化:如果增廣的時候,某一次重標記一個點的層次後,發現已經沒有哪個點的層次和這個點之前的那個層次一樣了(或者說某個層次上已經沒有點了),那麼 和 實際上已經不連通了,就再也找不到新的增廣路了。算法結束。
前面 Dinic 的一些常數優化顯然也同樣適用於 ISAP 。
實現的時候, ISAP 的 BFS 步驟實際上也可以用 DFS 重標號的形式來間接完成。不過速度可能會稍微降低(雖然我覺得是我寫錯了
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstdlib>
#include <cctype>
using namespace std;
#define getchar() (frS==frT&&(frT=(frS=frBB)+fread(frBB,1,1<<12,stdin),frS==frT)?EOF:*frS++)
#define add_edge(a,b,c) nxt[++tot] = head[a], head[a] = tot, to[tot] = b, cap[tot] = c
const int MAXN = 105, MAXM = 10005;
const int inf = 2147483647;
char frBB[1<<12], *frS = frBB, *frT = frBB;
int N, M, S, L, head[MAXN], nxt[MAXM], to[MAXM], dep[MAXN], gap[MAXN], tot = 1;
int cap[MAXM];
void read (int &x) {
x = 0; char ch = getchar();
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) x = x * 10 + (ch ^ 48), ch = getchar();
}
int flowrec;
int aug (const int &x, const int &flow) {
if (x == L) return flow;
int cost = 0;
for (register int i = head[x]; i; i = nxt[i]) {
if (dep[x] == dep[to[i]] + 1 && cap[i] > 0) {
flowrec = aug(to[i], min(flow - cost, cap[i]))
cap[i] -= flowrec, cap[i^1] += flowrec;
if ((cost += flowrec) == flow) return cost;
}
}
if (!--gap[dep[x]++]) dep[S] = N + 1;
return ++gap[dep[x]], cost;
}
int ta, tb, tc;
long long ans;
int main () {
read(N); read(M); read(S); read(L);
for (register int i = 1; i <= M; ++i) {
read(ta); read(tb); read(tc);
add_edge(ta, tb, tc);
add_edge(tb, ta, 0);
}
for (ans = 0, gap[0] = N; dep[S] <= N; ans += aug(S, inf));
return printf("%lld\n", ans), 0;
}
我覺得不太行。
這個算法叫做位縮放。大概思路:二進制拆分容量。從最高位開始、每次加一位然後試圖增廣填上多的容量。複雜度有點複雜,
之前介紹的都是增廣路算法;HLPP (最高標號預流推進)則是一種預流推進算法。
增廣路算法基於路徑/割;預流推進算法基於點。 HLPP 的理論複雜度比上面介紹的幾個要低,但是常數比較大,實際效果一般不是很好。
ps. 我下面寫嗨了,看着煩就跳過吧(
預流推進大概是這樣的:首先,下面講到的推進,比如說 push(u,v) ,僅當 u 的高度等於 v 的高度加 1 的時候纔會發生。
首先,我們從源點那裏流進去無限的流量;然後沿着出邊把流量推進給連着的點;接着把連着的點加入隊列,讓它們繼續把流量向匯點推進。假如在這個過程中,流入某個點的流量沒辦法全部流出,就讓某些入邊反向,把多出來的流量回流。注意:整個算法進行的過程中,跟增廣路算法不一樣,這裏只會往網絡中流入一次流量:在一開始的時候,從源點那裏流入。
但是如果只是這樣的話顯然可能會出現流量反覆橫跳的問題,所以得把有向邊用無向邊代替,具體說就是加入類似於層次圖的概念,不過在這裏變成了記錄高度。只允許“水往低處流”。這樣的話,前面的“讓某些入邊反向”就有了一個更明確的定義:把當前點的高度擡高,這樣自然就可以讓一些流量回流了,也算是達到了反向的效果吧?
需要注意的是,在這裏,某一個點上的流量只是暫時儲存的(補充一個概念:在某一個時刻,暫時儲存了流量的點叫做儲流點,或者 active node ),就是說我們不會保存在這個點這裏曾經流過了多少流量之類的,流走了就是流走了。
實際上講起來感覺挺自然的,要特意提是因爲前面增廣路算法裏面是一點一點流的,就得知道從某條邊上曾經一共流過了多少流量,才知道有沒有超過容量了;但是這裏不同,預流推進算法是預流的時候直接流滿,流到過載,再慢慢把過載的部分調回去,在這種調整的過程中,某條邊上流過的流量只會減少,不會增加,所以就不用考慮“某個點/邊上一共流過多少流量”了。
上面提到的推進過程中,如果一次推進使一條邊的流量達到容量,那麼稱這一次推進是 saturating ;否則稱爲 unsaturating push 。
預流推進算法的複雜度(用一般的 FIFO 隊列實現):
relabel:首先注意到除了源點和匯點不會被重新標號之外,每一個點的高度在算法進行的過程中一定只會變大,不會變小;並且每個點在算法進行的過程中可能達到的最大高度應該是 2n-1 。重標號的時間複雜度一共 。
Saturating Push:每條邊上都會正向 saturating push 次,同時最多會被反向 saturating push 次,一共最多可能發生 2n 次 saturating push 。顯然 Saturating Push 的時間複雜度一共 。
Unsaturating Push / Non-saturating Push:這個分析比較複雜。可以考慮用勢能分析,那麼首先要找到一個合適的勢能,並且我們容易發現在算法進行的過程中,所有 active node 的高度和初始爲 0,然後經過一系列變化最終歸於 0 。並且在這個變化的過程中, 所有 active node 的高度和一定不會是負數。記所有 active node 的高度 ℓ 的和爲 Φ ,前面分析過的 relabel 和 saturating push 兩種操作都只會讓 Φ 不減,那麼讓 Φ 歸零的任務就只有 unsaturating push 可以完成了。 現在回過頭來深入分析一下前面兩個操作對 Φ 的影響: relabel 不會改變 node 的 active 狀態,只會擡高 ℓ ,且一次 relabel 只會讓 Φ 增加 1 ,顯然 relabel 一共最多令 Φ 增大 。然後, saturating push 對 Φ 的影響是通過 activating inactive nodes 實現的,一次最多令 Φ 增大 2n-1 ,最多 saturating push 2nm 次,合計的話大約是令 Φ 增大 。又因爲 unsaturating push 一次最少令 Φ 減小 1 並且的確可能一直只讓 Φ 減少 1,所以 unsaturating push 的複雜度 。
預流推進算法有一個重要的優化,就是 cur 優化,延伸出一個 discharge 操作:類比前面講過的那種 cur ( Current-Arc ) 優化,容易發現 HLPP 要做 cur 優化,就必須強迫:走一條邊的話、就要把所有經過那條邊可以到達的地方都給走一遍,把那裏面所有的 saturating push 和 unsaturating push 都給處理完。這樣的話,走過的邊纔不需要再走。
HLPP 則採用優先隊列,每次從優先隊列/堆中取出高度最高的點推進。
Cheriyan 和 Maheswari 利用 trace (忽略細節來說是指 flow atom 的過去運動路徑)證明了採用 cur 優化的時候, 採用 Largest-Label 的預流推進算法的複雜度是 ; Levent Tuncel 則利用 future course (忽略細節來說是指 flow atom 的未來運動路徑)證明了不採用 cur 優化的情形下, HLPP 的複雜度同樣是 。
具體的複雜度證明可以參考 Tuncel, L. 「On the complexity of preflow-push algorithms for maximum-flow problems」。
前面分析 saturating push 和 unsaturating push 的複雜度的時候,講得有些模糊,最主要的原因就是進行一次操作的時候勢能具體的增減情況非常的不確定。所以考慮定義一個新的勢能。
Fʟoᴡ Aᴛoᴍ - Dᴇғɪɴᴀᴛɪoɴ
Ⅰ 對點 v 完成一次 relabel(v) 操作後,會在點 v 處產生一個 flow atom 。如果點 v 處本來就有一個 atom ,那麼舊的 atom 會消失。
Ⅱ 在 saturating push(u,v) 的時候, u 點上的 atom 會消失,然後在 u 點上產生一個新的 atom ,這個 atom 會沿着 (u,v) 移動到 v 點。如果在 saturating push 操作結束之後, u 點仍然 excess ,那麼在 u 點處會再產生一個新的 atom 。
Ⅲ 假如在 push(u,v) 操作進行之前 v 點就已經 excess ,那麼操作完成後會在 v 點產生一個 atom 。當然, v 點上如果本來就有 atom ,那麼在操作完成後 v 點上本來有的 atom 會消失。
根據之前的勢能分析,不難做出這樣的一個定義。分析 flow atom 顯然要比分析高度的變化要來得簡單一些。可以發現 flow atom 實際上就是僅隨着 unsaturating push 移動的 flow excess 。(如果 saturating push , atom 會 die ;如果 relable , atom 也會 die )
然後考慮 largest-label 時 unsaturating push 的複雜度。直接的勢能分析只能得到比較弱的上界,那可以考慮一下其它的複雜度分析方法:比如經常見到的那種,因爲對於路徑的重疊有限制,所以複雜度被圖的點數或者邊數限制一類的情況。
那首先就得有一個運動的概念。 flow atom 的定義Ⅱ裏面就有講到 flow atom 的移動,然後這裏再定義一個 future course 表示某個 flow atom 直到消失前的移動路徑。
注意:一、future couse 不考慮源點和匯點;二、如果一個 atom 移到一個點上就立刻消失了,那麼最後這一步移動不算在 future course裏面。
關於 future course 有什麼可以分析的馬?考慮一下兩個同時存在的 flow atom ,它們的 future course 會有交點嗎?——假設有,首先兩個 atom 不可能同時在那個交點上,不然就消失了,也就沒有交點了;其次,如果一個先一個後的話,先到那個交點上的點在到交點之後就不能移動了:因爲我們是遵循 Largest-Label 規則的,一個點到交點上了,另一個點顯然擁有一個更大的高度( label ),那麼這個時候就得先移動擁有 largest label 的點,然後兩個點還是會在一起,仍然不能相交。所以假設不成立。
也就是說同一時刻,網絡上任意兩個 flow atom 的 future course 不相交,並且 flow atom 的總數應該約是 的。一個 atom 的 future course 長度 ,假如直接用這個來算的話次數是 的 ;但是 label 是會變的,並且會影響到 future course 的長度,會影響到 push 的次數。
關於 label ,有一個結論: largest label 最多大約變化 4n2 次。論文沒有給出證明,我猜是因爲 relabel 最多大概 2n2 次,然後因爲採用 Largest-Label ,一次 relabel 最多隻能提供 2 次改變 largest label 的機會?(口胡
注意到之前對 unsaturatnig push 的次數分析裏面, 每一次產生的 flow atom 在非極端小數據的情況下, future course 的長度貌似不太可能一直都接近 。可以考慮將 future course 按照長度分類。
那麼 future course 可以分長度大於等於 ξ 的和長度小於等於 ξ 的情況來討論,顯然長度小於等於 ξ 的 future course 產生的 unsaturating push 最多 次。
長度大於等於 ξ 的 future course 就要被某一種 phase 限制:前面講了 largest label 最多大約變化 4n2 次,這個結論就可以在這裏利用,把一次 largest label of active nodes 的變化當作一個 phase 的結束和另一個 phase 的開始。一個 phase 中所有的 push 操作都會以擁有當前 phase 的 largest label 的那些點爲起點被執行。要把這些 future course 限制進同一個時刻的網絡裏,那麼可以考慮這些點是不是在某一個時刻全部都出現了:因爲採用的是 largest label ,那麼我們在這個 phase 裏面顯然不應該產生新的 label 等於該 phase 的 largest label 的點。那麼,一個 phase 裏面很顯然最多只能有 個長度大於等於 ξ 的 future course ,一共就有 個。
取一個 ξ 最小化 ,嘗試令 ,解得 。
所以 HLPP 的複雜度是 。
相對其它的最大流算法來說, HLPP 主要是對稠密圖特化的(畢竟它複雜度裏面的 m 帶了個根號,而無論是 Dinic + LCT 、 ISAP 、 還是 Orlin + KRT , 複雜度裏面都是完整的 m )。
HLPP 的核心部分代碼其實可以寫得比較短【參考】。不過跑隨機圖的話, ISAP 的常數太優秀了以至於遠遠達不到上界,所以基本上還是能夠吊打常數相對大的 HLPP ;而且 ISAP 的代碼還可以寫得更短……(當然特意構造的話還是能夠卡複雜度的 【參考】,不過最多也就是卡一下娛樂?)
PS. HLPP 顯然也可以用 GAP 優化。【參考】
關於理論複雜度低的最大流算法:首先,對於 oier/acmer 來說它們肯定是用不到的。 Orlin’s algorithm 的理論複雜度近 (雖然這個的價值遠不如 Spielman-Teng);也有複雜度接近 的算法【參考】。
另外,網絡流問題還可以用網絡單純形法(不是線性規劃那個單純形法)解決【參考】。網絡單純形法的速度非常快【參考】,當然實現起來也比較精污(一般來說最大流就 dinic 或者 isap ,費用流就 zkw)。如果有哪天必須得寫網絡單純形 + 動態樹,那我想出題人得先被吊起來打一頓(
最左轉線也叫做最小左轉法。
例題:WC2013. 平面圖 hnoi2016 礦區 ZJOI2008 Risk
只要求求出費用最小的流。連續spfa即可。
對費用先求一條最短路,然後繼續求最短路、如果求出來的最短路權和爲負就加上。
如果想要 dijkstra 就用 Johnson 那個方法,把所有邊 的權 重賦權爲 得到正權圖。
首先是一個最大流,然後要求費用最小。
一般來說用 EK (也有人叫做 SPFA 費用流)就夠了,基本上也不會有人專門去卡 EK 。分類討論和處理也不繁瑣,最多就是消負環。
如果你堅持擁護 spfa 死了就寫 dijkstra 吧。。。或者寫完 spfa 改成 dijkstra??至於寫起來有多噁心(每次搞都想婊一波卡 spfa 的,,,
反正大力上啊 。
寫帶優化 spfa 卡常數就別想了 跑負權一炸一個穩 優化多了還不如寫 dijkstra 。。。
如果當前費用是在當前流量下的最小費用,那麼以最小費用增廣之後的費用也爲增廣後的流量下的最小費用。不斷增廣找到的就是最小費用最大流。所以把費用作爲權值,每次跑最短路增廣直到最大流就可以了。記最大流爲 ,如果用 SPFA 找最短路(代替 BFS 找增廣路),用 Dijkstra 增廣,那麼複雜度爲 。
實際上,就像最小費用最大流問題是最大流問題的推廣,增廣路算法和預流推進算法都有一定的從最大流推廣到最小費用最大流問題上的可能。
慢。
消圈定理:一個流是這個流量下最小費用流的充要條件是殘量網絡中不存在負費用圈。
實現:先最大流求殘量網絡,這時候殘量網絡 S - T 可能有帶負權的增廣路,然後消負環。因爲消的是環,所以最大流不變;因爲消的是負,所以費用減小。
複雜度 。其中 是邊的最大容量, 是邊的最大費用。最大流費用不超過 ,而每次消去負費用圈至少使得費用減少 ,因此最多執行 次 的找 + 消負環。
關於費用流負邊、負環的處理:消圈算法比較慢。消除負環則可以不斷 SPFA 找負環,然後把整個負環上面所有邊的容量減去負環上容量最小的邊的容量。負環消除後,消負邊可以考慮重賦權【參考】, 求一次每個點到匯點的最短路,然後把每條邊 的 加上 再減去 得到 reduced cost ,用必定非負的 reduced cost 跑費用流算法。注意這樣跑出來的費用要進行調整。
reduce cost 之後,假如說之前這條邊是負權邊,那麼 reduce cost 之後它的權值爲 (因爲負環被消了, ;假如之前權非負,爲 ,那麼有 那麼 reduced cost 。並且在之後的增廣過程中不會產生新的負權邊:因爲每次增廣走的都是最短路上的邊。
做費用流的時候,增廣後要重新標號的原因是什麼?因爲增廣可能會破壞最短路,讓每個點的頂標失效;然後就得重新用最短路算法求出每個點到匯點的最短距離作爲合法的頂標。
然而被改動的頂標的數量是有限的。可以考慮直接重標號。類比 KM 算法,求每次 S-T 不連通時的 ,將這一輪中最後一次 DFS 訪問過的點的頂標 增加 ,然後重複增廣。
缺點挺明顯的。如果用來跑稀疏圖會爆炸。
連續最短路(SPFA + SLF) + 多路增廣(DFS) + Reduced Cost 。在 Reduced Cost 下, 一個是因爲邊權(指費用)縮小, SPFA 的速度會加快,並且除了第一次跑 SPFA 重賦權,後面都是在正權圖中跑 SPFA ,此時用 SLF 不會退化並且可能可以起到一定的玄學優化效果。實際操作起來因爲現在 SPFA 基本會隨手被卡…… .
正權圖非要跑 SPFA 是作死,更穩妥的話:第一次用 SPFA 處理掉負權。之後在 Reduced Cost 下都是正權圖就一律用 Dijkstra + Heap (要追求效率就手寫堆,偷懶就 priority_queue 。用 stl 的話寫起來 dij 跟 spfa 差不多長,雖然會讓常數變大)。正權圖也可以用 SPFA + SLF 跑,看臉。注意一下,因爲這裏負權圖只用跑一次就還無所謂,不然帶 SLF 跑負權圖可能爆炸。
注意一下 SLF 優化不要拿來跑負權圖,複雜度可能會退化。
Primal-Dual 某種程度上也許可以看成 isap 的推廣(?
樸素 Network Simplex 的論文:J. B. Orlin「A polynomial time primal network simplex algorithm for minimum cost flows」
LCT 優化:R. E. Tarjan「Dynamic trees as search trees via euler tours, applied to the network simplex algorithm」
工業用。不要想着考場上能夠碼這種東西((
- 無源匯有上下界可行流(循環流) -
- 有源匯有上下界可行流 -
- 有源匯有上下界最大流 -
可行流 + 最大流 即可。
- 有源匯有上下界最小流 -
- 有源匯有上下界費用流 -
一個無向圖是二分圖的充要條件:不存在奇環。
二分圖最大匹配 = 最大流。
Dinic 證明了 Dinic’s algorithm 求最大匹配的時候最多增廣次,也就是說這種情況下它的時間複雜度是 。
這時候的 Dinic’s algorithm 也被稱爲 Hopcroft–Karp algorithm 。
Dinic’s algorithm 跑二分圖最大匹配曾經是理論複雜度墜吼的那個,不過之後被預流推進翻了。當然實際上寫還是寫 Dinic 好吧(
二分圖最大權匹配 = 最大費用流。
二分圖最小點覆蓋 = 最大費用流。
König 定理:最小點覆蓋 = 最大匹配
BZOJ1143
任意圖最大獨立集 = 點數 - 最小點覆蓋。
https://yzmduncan.iteye.com/blog/1149057
任意圖最大點權獨立集 = 點權和 - 最小點權覆蓋。
無向最大團 = 補圖最大獨立集
無向圖最大獨立集 = 補圖最大團
最大權閉合子圖 = 正點權和 - 最小割
- 一般形式 -
無向圖子圖密度:
轉化成分數規劃 +最小割。
- 點權 -
- 點權 & 邊權 -
帶花樹。
一個特殊情況是平面圖中的全局最小割,