網路最大流演算法之Ford_Fullkerson方法,EK演算法c++模板
該演算法最精華的部分是反向邊的理解,
即修改容量的時候為什麼反向邊加上該值,
c[pre[i]][i]-=_min;
c[i][pre[i]]+=_min;
在演算法導論中對求解最大流問題給出了一般性的解決方法,但並沒有涉及到具體的實現。在這裡我還是重新的對求解最大流的思想進行一般性的描述,然後再給出具體的實現。
Ford-Fulkerson方法依賴於三種重要思想,這三個思想就是在上一篇網路流基礎中提到的:殘留網路,增廣路徑和割。Ford-Fulkerson方法是一種迭代的方法。開始時,對所有的u,v∈V有f(u,v)=0,即初始狀態時流的值為0。在每次迭代中,可通過尋找一條“增廣路徑”來增加流值。增廣路徑可以看成是從源點s到匯點t之間的一條路徑,沿該路徑可以壓入更多的流,從而增加流的值。反覆進行這一過程,直至增廣路徑都被找出來,根據最大流最小割定理,當不包含增廣路徑時,f是G中的一個最大流。在演算法導論中給出的Ford-Fulkerson實現程式碼如下:
FORD_FULKERSON(G,s,t)
1 for each edge(u,v)∈E[G]
2 do f[u,v] <— 0
3 f[v,u] <— 0
4 while there exists a path p from s to t in the residual network Gf
5 do cf(p) <— min{ cf(u,v) : (u,v) is in p }
6 for each edge(u,v) in p
7 do f[u,v] <— f[u,v]+cf(p) //對於在增廣路徑上的正向的邊,加上增加的流
8 f[v,u] <— -f[u,v] //對於反向的邊,根據反對稱性求
第1~3行初始化各條邊的流為0,第4~8就是不斷在殘留網路Gf中尋找增廣路徑,並沿著增廣路徑的方向更新流的值,直到找不到增廣路徑為止。而最後的最大流也就是每次增加的流值cf(p)之和。在實際的實現過程中,我們可以對上述程式碼做些調整來達到更好的效果。如果我們採用上面的方法,我們就要儲存兩個陣列,一個是每條邊的容量陣列c,一個就是上面的每條邊的流值陣列f,在增廣路徑中判斷頂點u到v是否相同時我們必須判斷c[u][v]-f[u][v]是否大於0,但是因為在尋找增廣路徑時是對殘留網路進行查詢,所以我們可以只儲存一個數組c來表示殘留網路的每條邊的容量就可以了,這樣我們在2~3行的初始化時,初始化每條邊的殘留網路的容量為G的每條邊的容量(因為每條邊的初始流值為0)。而更新時,改變7~8行的操作,對於在殘留網路上的邊(u,v)執行c[u][v]-=cf(p),而對其反向的邊(v,u)執行c[v][u]+=cf(p)即可。
現在剩下的最關鍵問題就是如何尋找增廣路徑。而Ford-Fulkerson方法的執行時間也取決於如何確定第4行中的增廣路徑。如果選擇的方法不好,就有可能每次增加的流非常少,而演算法執行時間非常長,甚至無法終止。對增廣路徑的尋找方法的不同形成了求最大流的不同演算法,這也是Ford-Fulkerson被稱之為“方法”而不是“算法”的原因。下面將給出Ford-Fulkerson方法的具體實現細節:
int c[MAX][MAX]; //殘留網路容量
int pre[MAX]; //儲存增廣路徑上的點的前驅頂點
bool visit[MAX];
int Ford_Fulkerson(int src,int des,int n){ //src:源點 des:匯點 n:頂點個數
int i,_min,total=0;
while(true){
if(!Augmenting_Path(src,des,n))return total; //如果找不到增廣路就返回,在具體實現時替換函式名
_min=(1<<30);
i=des;
while(i!=src){ //通過pre陣列查詢增廣路徑上的邊,求出殘留容量的最小值
if(_min>c[pre[i]][i])_min=c[pre[i]][i];
i=pre[i];
}
i=des;
while(i!=src){ //再次遍歷,更新增廣路徑上邊的流值
c[pre[i]][i]-=_min;
c[i][pre[i]]+=_min;
i=pre[i];
}
total+=_min; //每次加上更新的值
}
}
Edmonds-Karp演算法實際上就是採用廣度優先搜尋來實現對增廣路徑的p的計算,程式碼如下:
bool Edmonds_Karp(int src,int des,int n){
int v,i;
for(i=0;i<n;i++)visit[i]=false;
front=rear=0; //初始化
que[rear++]=src;
visit[src]=true;
while(front!=rear){ //將源點進隊後開始廣搜的操作
v=que[front++];
//這裡如果採用鄰接表的連結串列實現會有更好的效率,但是要注意(u,v)或(v,u)有任何一條
//邊存在於原網路流中,那麼鄰接表中既要包含(u,v)也要包含(v,u)
for(i=0;i<n;i++){
if(!visit[i]&&c[v][i]){ //只有殘留容量大於0時才存在邊
que[rear++]=i;
visit[i]=true;
pre[i]=v;
if(i==des)return true; //如果已經到達匯點,說明存在增廣路徑返回true
}
}
}
return false;
}
完整版程式碼,我覺得寫得很清楚,可以用鄰接表優化:
[cpp] view plain copy print?- #include<iostream>
- #include<queue>
- using namespace std;
- const int maxn=205;
- const int inf=0x7fffffff;
- int r[maxn][maxn]; //殘留網路,初始化為原圖
- bool visit[maxn];
- int pre[maxn];
- int m,n;
- bool bfs(int s,int t) //尋找一條從s到t的增廣路,若找到返回true
- {
- int p;
- queue<int > q;
- memset(pre,-1,sizeof(pre));
- memset(visit,false,sizeof(visit));
- pre[s]=s;
- visit[s]=true;
- q.push(s);
- while(!q.empty())
- {
- p=q.front();
- q.pop();
- for(int i=1;i<=n;i++)
- {
- if(r[p][i]>0&&!visit[i])
- {
- pre[i]=p;
- visit[i]=true;
- if(i==t) return true;
- q.push(i);
- }
- }
- }
- return false;
- }
- int EdmondsKarp(int s,int t)
- {
- int flow=0,d,i;
- while(bfs(s,t))
- {
- d=inf;
- for(i=t;i!=s;i=pre[i])
- d=d<r[pre[i]][i]? d:r[pre[i]][i];
- for(i=t;i!=s;i=pre[i])
- {
- r[pre[i]][i]-=d;
- r[i][pre[i]]+=d;
- }
- flow+=d;
- }
- return flow;
- }
- int main()
- {
- while(scanf("%d%d",&m,&n)!=EOF)
- {
- int u,v,w;
- memset(r,0,sizeof(r));///
- for(int i=0;i<m;i++)
- {
- scanf("%d%d%d",&u,&v,&w);
- r[u][v]+=w;
- }
- printf("%d\n",EdmondsKarp(1,n));
- }
- return 0;
- }