跳到主要內容

演算法

同一個問題,不同的解題思路

主題 · 最大流與二分匹配

最大流與二分匹配

一個管線網路最多能送多少水?Ford-Fulkerson 不斷找還有剩餘容量的路徑;同一套方法也能解工作分配這類二分匹配問題。

網路
邊上顯示
1/8
0/130/40/90/140/70/40/160/120/20sabcdt
增廣路徑逆著邊走:把流量退回最小割(虛線)割的起點這一側

每條邊寫的是「流量/容量」,粗體代表已經滿了。

在剩餘圖上做 BFS(Breadth-First Search,廣度優先搜尋),找到邊數最少的路:s → a → c → t。最窄的一段還剩 12,所以可以再送 12。

總流量
0
增廣次數
0
最小割容量
…
亮起來的是這一步執行的程式碼
// Returns the maximum flow and the source side of a minimum cut.
function edmondsKarp(cap: number[][], s: number, t: number): { flow: number; cut: number[] } {
const n = cap.length;
const flow = cap.map((row) => row.map(() => 0));
let total = 0;
for (;;) {
const parent = new Array(n).fill(-1);
parent[s] = s;
const queue = [s];
while (queue.length && parent[t] === -1) {
const u = queue.shift()!;
for (let v = 0; v < n; v++) {
if (parent[v] === -1 && cap[u][v] - flow[u][v] > 0) {
parent[v] = u;
if (v === t) break;
queue.push(v);
}
}
}
if (parent[t] === -1) {
// No path left: what is still reachable is the source side of a minimum cut.
const cut = parent.flatMap((p, v) => (p === -1 ? [] : [v]));
return { flow: total, cut };
}
let bottleneck = Infinity;
for (let v = t; v !== s; v = parent[v])
bottleneck = Math.min(bottleneck, cap[parent[v]][v] - flow[parent[v]][v]);
for (let v = t; v !== s; v = parent[v]) {
flow[parent[v]][v] += bottleneck;
flow[v][parent[v]] -= bottleneck;
}
total += bottleneck;
}
}

模型假設與範圍

  • Big O 與實測步數以頁面列出的基本操作為單位;視覺化的快照、畫圖、程式高亮和輸出成本另計,不是執行時間 benchmark。

什麼時候用

  • 有容量限制的網路,問「最多能送多少」:水管、頻寬、道路、物流。
  • 二分配對:兩群東西兩兩配對(工人和工作、學生和學校),加上起點和終點、容量全設 1,最大流量就是最多能配幾對。
  • 最小割:要找「切斷哪些邊花費最少就能把兩邊分開」,例如網路最脆弱的地方、影像分割的前景背景。

和其他主題的關係

由這些組成
BFS 與 DFS
延伸閱讀
最小生成樹

時間與空間複雜度(Big O)

操作平均最差
Edmonds-Karp
最多 O(VE) 次增廣,每次一趟 BFS O(E);實際上通常少很多
O(VE²)O(VE²)
容量都是整數時
f 是最大流量:每次至少多 1
O(E · f)O(E · f)
二分配對(Hopcroft-Karp)O(E√V)O(E√V)

空間:O(V + E),剩餘容量和 BFS 的父節點;示範用的是 O(V²) 的矩陣

Big O 實測:n 變大時步數怎麼長

數的是:增廣次數(n 個工人、n 份工作,每人會 3 份)

Big On = 10n = 20n = 40n = 80成長倍數:實測(理論)
Edmonds-Karp:增廣次數O(n)10194077×7.7 (×8.0)

容量都是 1,每次增廣只多配一對,次數就跟配對數一樣隨 n 線性增長;每次 BFS 本身的成本也跟著變大,總工作量見上方表格。

和其他做法比

邊數最大流量增廣次數(BFS 次數 − 1)BFS 檢查的格子
水管網路(示範)9233102
配對:10 個工人、10 份工作5010102,222
配對:20 個工人、20 份工作100191915,582
配對:40 個工人、40 份工作200404093,972
配對:80 個工人、80 份工作4007777757,998

配對問題裡每個工人會做隨機 3 份工作,容量全是 1,所以每條增廣路徑只多配一對:增廣次數等於配到的對數,跟 n 成正比。每次 BFS 掃的是 (2n + 2) × (2n + 2) 的容量矩陣,所以總工作量大約是 n 的三次方:n 變 8 倍、格子變三百多倍。改用鄰接串列,一次 BFS 是 O(V + E);Hopcroft-Karp 一次 BFS 找好幾條路,配對可以做到 O(E√V)。

真實世界裡的它

  • 航空公司排機組人員、醫院排班、把廣告分配到版位。
  • 電腦視覺的影像分割(graph cut):像素是點,最小割把前景和背景切開。
  • 棒球淘汰問題:某隊就算剩下的比賽全勝,還有沒有機會拿第一?可以化成一個流量問題。

取捨與陷阱

  • 忘了反向邊:只沿著原本的方向找路,會卡在第一次選錯的路線上,得到比最大值小的答案。剩餘圖的反向邊讓演算法能「反悔」。
  • 用 DFS 隨便找一條增廣路(Ford-Fulkerson 原始版):容量很大時次數可能和容量成正比,容量是無理數時甚至不會停。用 BFS 找最短的路就保證 O(VE²)。
  • 最小割要在剩餘圖上找起點走得到的點,不是在原圖上;而且割的容量只算從起點這側出去的邊。

LeetCode 練習