Maximum-Flow
Maximum-Flow
中文叫做最大流,什麼是流?可以把一個圖上邊看作為水管,其權重看作為水流能夠通過的量
若圖為有向圖代表水流只允許單向流動,若為無向圖則代表可以同時往兩邊流動
而最大流是在一個圖中,在所有可能的 s-t 流(從源點 s 到 匯點 t)尋找最大值
要找到最大流可以使用 Ford-Fulkerson 演算法
但是 Ford-Fulkerson 演算法中,會因為其沒有對尋找增廣路徑的策略限制,有可能會導致不斷重複選擇非最佳解
所以我們這篇筆記主要會以 Ford-Fulkerson 的改良版:Edmonds-Karp 為主
要點
Ford-Fulkerson 觀念
s-t 流的過程會由許多大大小小的支流匯聚
演算法的目標就是不斷的尋找最適合的支流(也就是增廣路徑)並進行加總
該如何尋找增廣路徑呢?
先找一條可以從 s 走到 t 的路徑
找到這條路徑中最少的水管流量
將這條路徑上的所有水管縮減該流量,如果縮減後流量為零則刪除該水管
最後將該流量加到總和
但是在尋找路徑的過程中,有機會遇到找到不是最適合的路徑
因此需要有一個回溯機制
此機制可以透過建立反向流量來實現
在縮減所有水管的流量時,順便建立一條反向的同流量可以使未來的路徑連回過去的路徑
此部分透過底下的圖來解釋
Edmonds-Karp 觀念
在 Ford-Fulkerson 中,有可能因為一直找到不合適的路徑而耗費大量時間
這時候可以透過限制每次尋找的路徑為最短路徑來解決上述問題
所以用 BFS 尋找最短路徑可以解決 Ford-Fulkerson 的缺點
這就是 Edmonds-Karp 演算法
Edmonds-Karp 要點
需要一個能夠紀錄路徑的 BFS,因此需要一維陣列紀錄拜訪過的節點在路徑中的父節點
並且需要一個二維陣列以鄰接矩陣的方式儲存圖
相較於鄰接串列,鄰接矩陣更容易還原路徑
實作
於全域宣告,使 BFS 函式可以使用
1
2
3
int n, s, e, t, a, b, w; // 節點數, 起點, 終點, 邊數, 圖, 拜訪紀錄, 路徑紀錄
vector<vector<int>> g;
vector<int> vis, par;BFS 函式,當找到一條 s-t 路徑時,回傳 true
找不道路徑時,回傳 false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool BFS() {
queue<int> q;
vis.assign(n+1, 0); par.assign(n+1, 0);
q.push(s);
while (q.size()) {
int now = q.front(); q.pop();
if (now == e) return true;
for (int i = 1; i <= n; i++) if (g[now][i] && !vis[i]) { // g[now][i] 判斷兩節點之間的剩餘流量是否大於零
vis[i] = 1;
par[i] = now; // 紀錄路徑
q.push(i);
}
}
return false;
}在主函式中建圖
且 g[節點][節點] 的 value 為邊的權重,也就是水管的流量
1
2
3
4
5
6
g.assign(n+1, vector<int>(n+1, 0));
cin >> s >> e >> t;
while (t--) {
cin >> a >> b >> w;
g[a][b] += w;
}Edmonds-Karp 本體
如果 BFS 有找到 s-t 的路徑
就利用找到的路徑(紀錄於 par)進行計算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int ans = 0; // 最大流計數器
while (BFS()) { // 有無找到 s-t 的路徑
int now = e, mnF = INF; // 從終點開始,#define INF 0x3f3f3f3f
while (now != s) { // 回推到起點
mnF = min(mnF, g[par[now]][now]); // 尋找路徑中最小的流量
now = par[now]; // 移動至上一個節點
}
now = e; // 回到終點
while (now != s) { // 從終點回推到起點
g[par[now]][now] -= mnF; // 縮減水管的流量
g[now][par[now]] += mnF; // 建立反向流量
now = par[now];
}
ans += mnF; // 最大流加上該流量
}
cout << "Max-Flow is " << ans; // 輸出加總,為最大流