網路最大流是指在一個網路流圖中可以從源點流到匯點的最大的流量。求解網路最大流的常用演算法可以分為增廣路徑演算法和預推進演算法。其中,預推進演算法的理論複雜度優於增廣路徑演算法,但是編碼複雜度過高,且效率優勢在很多時候並不是很明顯,因此,經常使用的演算法為增廣路徑演算法。
增廣路徑演算法主要有Fold-Fulkerson演算法,Edmonds-Karp演算法,Dinic演算法,ISAP演算法。其中,Fold-Fulkerson 是最基本的增廣路徑思想,不能算作嚴格的演算法實現。
增廣路徑
增廣路徑演算法的思想是每次從源點出發找到一條到達匯點的可行路徑,那麼從源點到匯點的網路流至少可以增加w(w為這條路徑上的邊的最小容量)。此時,將最大流增加w,這條路徑稱為增廣路徑,同時從源到匯沿著增廣路徑將經過的每條正向邊(從源指向匯)的流量都減去w,並將每條邊的反向邊的流量加上w。這個操作就為增廣操作。
不斷的進行增廣操作,直到無法從源到達匯停止。那麼,此時得到最大流的流量。同時,可以得到在獲得最大流的時候,每條邊上的流量分佈(只需要將原圖中每條邊的容量減去最後的殘餘網路
中每條邊對應的流量即可)。
殘餘網路
在增廣路徑的過程中每次進行增廣操作之後,得到的新圖稱為舊圖的殘餘網路。
1. Fold-Fulkerson演算法
Fold-Fulkerson演算法就是樸素的增廣路徑思想。
求最大流的過程,就是不斷找到一條從源到匯的路徑,然後構造殘餘網路,再在殘餘網路的基礎上尋找新的路徑,使總流量增加,然後形成新的殘餘網路,在尋找新路徑.... 直到某個殘餘網路上找不到從源到匯的路徑為止。
每用DFS執行一次找路徑,增廣的操作,都會使得最大流增加,假設最大流為C,那麼時間複雜度可以達到 C*(m+n), m為邊的數目,n為頂點的數目。
2. Edmonds-Karp演算法
Edmonds-Karp演算法是在Fold-Fulkerson思想上進行改進:
每次尋找增廣路徑的時候,總是尋找一條從源到匯經過節點數目最少的路徑,即最短路徑。這是一種“最短增廣路徑” Shortest Augmenting Path(SAP)的演算法。
在實現的時候,每次利用BFS搜尋,找到一條從源到匯的最短路徑,然後進行增廣操作;再進行BFS....直到無法找到從源到匯的路徑為止。
時間複雜度可以達到 n*m*m, n為頂點的數目,m為邊的數目。
EdmondsKarp演算法的實現
//增廣操作 int Augment(int s, int t, int n){ memset(gVisited, false, sizeof(gVisited)); gVisited[s] = true; int find_path = false; queue<int>Q; Q.push(s); while (!Q.empty()){ int u = Q.front(); Q.pop(); //廣度優先搜尋一條從源點到匯點的路徑 for (int i = 0; i < n; i++){ if (gGraph[u][i] > 0 && !gVisited[i]){ gVisited[i] = true; gPre[i] = u; if (i == t){ find_path = true; break; } Q.push(i); } } } if (!find_path) return 0; int min_flow = 1 << 28; int u = t; //尋找路徑上最小的容量 while (u != s){ int v = gPre[u]; min_flow = min_flow < gGraph[v][u] ? min_flow : gGraph[v][u]; u = v; } u = t; //進行增廣操作 while (u != s){ int v = gPre[u]; gGraph[v][u] -= min_flow; gGraph[u][v] += min_flow; u = v; } return min_flow; }
int main(){
//buildgraph
int aug;
int max_flow = 0;
while (aug = Augment(s, t, n)){
max_flow += aug;
}
return 0;
}
3. Dinic演算法
Edmonds-Karp演算法每找到一條增廣路徑進行增廣操作之後都再次回到原點重新進行BFS,這樣效率較低。Dinic演算法是在找到一條增廣路徑,增廣操作完之後,並不回溯到源點,而是回到距離源點最近的邊上流量為0的邊的起點。因為每次增廣操作,都會使得增廣路徑上的某些邊的流量變為0,這樣這條增廣路徑上的某些邊就無法再走,需要回溯,但是回溯並不需要回溯到源點,只需要回溯到一點A,使得從源點到點A的路徑上的流量不為0,且點A儘可能靠近匯點(為了遍歷所有情況)即可。
具體的演算法流程是:
1. 先用BFS對網路進行分層,分層是指按照距離源點的最近距離大小對各個點進行標號。
2. 然後利用DFS從前一層向後一層反覆 每次選擇增廣路徑的時候,從點u總是選擇滿足關係 dist[v] = dist[u] + 1的點v,這樣u->v的路徑肯定屬於某條最短路。
3. 找到一條增廣路徑之後進行增廣操作
4.路增廣操作之後進行回溯,將點u回溯到點A,使得從源點到點A的路徑上流量不為0,且點A儘可能靠近匯點徑5. 從點u開始繼續選擇可行點v(滿足 dist[v] = dist[u] + 1),直到匯點,這樣就又找到一條增廣路徑....
6. 直到點u回溯到源點,再回到1繼續操作,直到在分層操作時,無法用BFS找到從源到匯的路徑。
Dinic 演算法的實現
bool gVisited[N]; int gLayer[N]; int gGraph[N][N]; //將節點進行分層 bool CountLayer(int s, int t, int n){ deque<int> dQ; memset(gLayer, 0xFF, sizeof(gLayer)); dQ.push_back(s); gLayer[s] = 0; while (!dQ.empty()){ int v = dQ.front(); dQ.pop_front(); for (int i = 0; i < n; i++){ if (gLayer[i] == -1 && gGraph[v][i] > 0){ gLayer[i] = gLayer[v] + 1; if (i == t) return true; dQ.push_back(i); } } } return false; } int Dinic(int s, int t, int n){ int max_flow = 0; deque<int> dQ; while (CountLayer(s, t, n)){ dQ.push_back(s); memset(gVisited, false, sizeof(gVisited)); gVisited[s] = true; while (!dQ.empty()){ int v = dQ.front(); dQ.pop_front(); if (v == t){ int min_flow = 1 << 29; int min_vs = 0; for (int i = 0; i < dQ.size() - 1; i++){ int vs = dQ[i]; int ve = dQ[i + 1]; if (min_flow > gGraph[vs][ve]){ min_flow = gGraph[vs][ve]; min_vs = vs; } } //增廣路徑 max_flow += min_flow; for (int i = 0; i < dQ.size() - 1; i++){ int vs = dQ[i]; int ve = dQ[i + 1]; gGraph[vs][ve] -= min_flow; gGraph[ve][vs] += min_flow; } //回找到 min_flow的層次最小的節點位置 while (!dQ.empty() && dQ.back() != min_vs){ gVisited[dQ.back()] = false; dQ.pop_back(); } } else{ int i = 0; for (i = 0; i < n; i++){ if (!gVisited[i] && gGraph[v][i] > 0){ gVisited[i] = true; dQ.push_back(i); break; //找到一條可以繼續走的路,就繼續往下走。因為使用棧,所以break, } } if (i == n) //如果在這一層找不到往下走的路,進行回溯 dQ.pop_back(); } } } return max_flow; }
4. ISAP演算法
ISAP演算法為優化的最短增廣路徑演算法(Improved Shortest Augmenting Path)。相比Dinic,ISAP演算法不需要在回溯到源點之後再次進行BFS分層操作,而是在DFS以及回溯的過程中就進行了節點的重標號(即分層操作);以及ISAP演算法進行gap優化大大提升效率。
具體流程為:
1. 定義dist[v] 為點v到達匯點的距離(即經過幾個點到達匯點),定義gap[d]在當前殘餘網路中到達匯點(經過路徑上流量不能為0)距離為d的點的個數。
2. 從匯點到源點進行BFS,標記下每個節點到達匯點的最短距離,即和Dinic演算法相反的分層操作。
3. 當前點u從源點出發,用DFS找到一條到達匯點的路徑....
4. 若點u為匯點,則找到了一條增廣路徑,進行增廣操作;若點u可以向前走到v,且u-->v為一條可行邊(dist[u] = dist[v]+1,且邊u-->v流量不為0),則u走到v;若u無法向前推進到任何點,則對u進行重標號,然後回溯u到原來的增廣路徑中上一個點 pre[u].
5. 在重標號和回溯之前
,可以進行gap優化。gap[d]在當前殘餘網路中到達匯點(經過路徑上流量不能為0)距離為d的點的個數。
若從u無法找到一條可行邊,則表明可以經過 dist[u] 條邊到達匯點的點數目少了一個,即 gap[dist[u]] --。若此時 gap[dist[u]] = 0,則說明當前殘餘網路中,沒有任何一個點可以經過dist[u]條邊到達匯點,源點到匯點的距離肯定大於等於dist[u],若源點能夠到達匯點,那麼要求源點到匯點的路徑中肯定有到達匯點距離為dist[u]的點,所以,無法從源點到達匯點,此時,可以直接返回結果。
6. 重標號,是將點u重新分層,重新設定點u經過不為0的邊可達匯點的最短距離。具體是 dist[u] = min{dist[v]|u連線到v,且u-->v邊流量不為0} + 1. 若從u出發的邊流量均為0,則無法找到下一個點,則直接將dist[u]置為n(n為節點個數),這樣就說明u點不可達。
ISAP演算法的實現(c++)
#include<stdio.h>
#include<string.h>
#include<iostream>
#include<vector>
#include<queue>
#include<map>
#include<algorithm>
using namespace std;
#define INFINITE 1 << 28
#define MAX_NODE 205
#define MAX_EDGE_NUM 500
#define min(a,b) a < b?a:b
struct Edge{
int from; //起點
int to; //終點
int w;
int next; //從from 出發的,下一條邊的序號
int rev; //該邊的反向邊 序號
//用於查詢反向邊
bool operator == (const pair<int,int>& p){
return p.first == from && p.second == to;
}
};
Edge gEdges[MAX_EDGE_NUM];
int gHead[MAX_NODE];
int gPre[MAX_NODE];
int gPath[MAX_NODE];
int gGap[MAX_NODE];
int gDist[MAX_NODE];
int gFlow[MAX_NODE][MAX_NODE];
int gSource, gDestination;
int gEdgeCount;
void InsertEdge(int u, int v, int w){
Edge* it = find(gEdges, gEdges + gEdgeCount, pair<int, int>(u, v));
if (it != gEdges + gEdgeCount){ //如果已經有邊 u --> v,則之前肯定已經指定了 u-->v 和 v-->u的反向關係
it->w += w;
}
else{ //新增 u-->v的邊和反向邊 v --> u
int e1 = gEdgeCount;
gEdges[e1].from = u;
gEdges[e1].to = v;
gEdges[e1].w = w;
gEdges[e1].next = gHead[u];
gHead[u] = e1;
gEdgeCount++;
int e2 = gEdgeCount;
gEdges[e2].from = v;
gEdges[e2].to = u;
gEdges[e2].w = 0;
gEdges[e2].next = gHead[v];
gHead[v] = e2;
//指定各個邊的反向邊
gEdges[e1].rev = e2;
gEdges[e2].rev = e1;
gEdgeCount++;
}
gFlow[u][v] = w;
}
//使用bfs進行分層,標記每個點到終點的距離
void Bfs(){
memset(gGap, 0, sizeof(gGap));
memset(gDist, -1, sizeof(gDist));
queue<int> Q;
gGap[0] = 1;
gDist[gDestination] = 0;
Q.push(gDestination);
while (!Q.empty()){
int n = Q.front();
Q.pop();
for (int e = gHead[n]; e != -1; e = gEdges[e].next){
int v = gEdges[e].to;
if (gDist[v] >= 0){ //gDist初始值為-1. 如果>= 0,說明之前已經被訪問過了
continue;
}
gDist[v] = gDist[n] + 1;
gGap[gDist[v]] ++;
Q.push(v);
}
}
}
int ISPA(int n){ //n 為頂點的個數
int ans = 0, u = gSource, d, e;
while (gDist[gSource] <= n){
if (u == gDestination){//進行增廣
int min_flow = INFINITE;
for (e = gPath[u]; u != gSource; e = gPath[u = gPre[u]]) //找到路徑中最小的邊流量
min_flow = min(min_flow, gEdges[e].w);
for (e = gPath[u = gDestination]; u != gSource; e = gPath[u = gPre[u]]){ //增廣操作
gEdges[e].w -= min_flow;
gEdges[gEdges[e].rev].w += min_flow;
gFlow[gPre[u]][u] += min_flow;
gFlow[u][gPre[u]] -= min_flow;
}
ans += min_flow;
}
for (e = gHead[u]; e != -1; e = gEdges[e].next){
if (gEdges[e].w > 0 && gDist[u] == gDist[gEdges[e].to] + 1)
break;
}
if (e >= 0){ //可以向前找到一點,繼續擴充套件
gPre[gEdges[e].to] = u;
gPath[gEdges[e].to] = e;
u = gEdges[e].to;
}
else{
if (--gGap[gDist[u]] == 0){ //gap 優化
break;
}
for (d = n, e = gHead[u]; e != -1; e = gEdges[e].next){ //重標號
if (gEdges[e].w > 0)
d = min(d, gDist[gEdges[e].to]);
}
gDist[u] = d + 1;
++gGap[gDist[u]];
if (u != gSource) //回溯
u = gPre[u];
}
}
return ans;
}
int main(){
int u, v, w;
int n, m;
while (scanf("%d %d", &m, &n) != EOF){
gEdgeCount = 0;
memset(gHead, -1, sizeof(gHead));
for (int i = 0; i < m; i++){
scanf("%d %d %d", &u, &v, &w);
InsertEdge(u, v, w);
}
gSource = 1;
gDestination = n;
Bfs();
int result = ISPA(n);
printf("%d\n", result);
}
return 0;
}