文章出自:http://dsqiu.iteye.com/blog/1689507
有向強連通和網絡流大講堂——史無前例最大流(最小割)、最小費用最大流
本文內容框架(未完成):
§1網絡流的基本概念
§2最大流問題
§2.1Ford-Fulkerson方法(增大路徑最大流算法)
§2.2Edmonds-Karp(EK)算法實現
§2.3Dinic算法
§2.4SAP算法(最短路徑增廣算法)
§2.5Preflow push method(預流-推進最大流方法)
§3最小費用最大流問題
§4有向圖強連通分量的問題
Kosaraju算法、Tranjan算法、Gabow算法
§5問題歸約
§6小結
§1網絡流的基本概念
╔
一、流網絡:網絡或容量網絡指的是一個連通的賦權有向圖 D= (V、E、C) , 其中V 是該圖的頂點集,E是有向邊(即弧)集,C是弧上的容量。此外頂點集中包括一個起點(源點)s和一個終點t(匯點)。網絡上的流就是由起點流向終點的可行流,這是定義在網絡上的非負函數,它一方面受到容量的限制,另一方面除去起點和終點以外,在所有中途點要求保持流入量和流出量是平衡的。
二、流:設G(E,V)是一個流網絡,那麼G的流是定義在V*V上的一個實值函數f,並且滿足一下三個性質:
1)容量限制:f(u,v)<=c(u,v)
2)反對稱性:f(u,v)=-f(v,u)
3)流守恆性:簡單點說就是:對於非源點,它流出的流量總和爲0,對於非匯點,流入它的流量總和爲0
╝①
§2最大流問題
╔
那麼給定一個流網絡,最大流問題就是求該流網絡中,原點流出的最大流量或者是流入匯點的最大流量。如何求解這個問題:要引入一下三個概念。
一、殘餘網絡:在一個流網絡中,給定一個流,那麼我們可以根據這個流計算出對應的殘餘網絡cf(u,v)=c(u,v)-f(u,v),值得注意的一點就是,如果f(u,v)爲負,那麼cf(u,v)就大於c(u,v)
二、增廣路徑:這個其實很簡單,在一個殘餘網絡中,任何一條從源點S到匯點T的路徑(cf(u,v)>0)都是增廣路徑。一條增廣路徑的流量是路徑中cf(u,v)中的最小值。
三、網絡流的割:流網絡G(E,V)的割(S,T)將流網絡分解爲S,T(V-S)兩個部分,其中源點s屬於S,匯點t屬於T。於是我們定義割(S,T)的淨流f(S,T)和容量c(S,T).其中f(S,T)爲從S到T的流量總和減去從T到S的流量總和。而c(S,T)爲從S到T的容量總和。
基於以上三個概念,有如下總要定理:
最大流最小割定理
對於一個流網絡G=(E,V),一下三個命題兩兩等價
1)f是G的一個最大流
2)殘餘網絡Gf不存在增廣路徑
3)對於G的某個割(S,T),f=c(S,T)且c(S,T)是G的某個最小割
╝①
§2.1Ford-Fulkerson方法(增大路徑最大流方法)
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-FullKerson方法是一種方法而不是一種算法,也許是因爲其沒有給出具體尋找增廣路徑的方法。
Ford-Fulkerson算法實現
Ford-Fulkerson算法使用DFS查找增廣路徑,下面附上兩個版本的實現,大同小異
╔
- #include<stdio.h>
- #include<string.h>
- #define maxn 16
- #define inf 999999
- int s,t,i,n,m,u,v,w,flow,maxflow;
- int vis[maxn];
- int c[maxn][maxn];
- int dfs(int u,int low)
- {
- int i,flow;
- if(u==t)
- return low;
- if(vis[u])
- return 0;
- vis[u]=1;
- for(i=1;i<=n;i++)
- if(c[u][i]&&(flow=dfs(i,low<c[u][i]?low:c[u][i])))
- {
- c[u][i]-=flow;
- c[i][u]+=flow;
- return flow;
- }
- return 0;
- }
- int main()
- {
- int ca,k=1;
- scanf("%d",&ca);
- while(ca--)
- {
- scanf("%d%d",&n,&m);
- memset(c,0,sizeof(c));
- memset(vis,0,sizeof(vis));
- for(i=1;i<=m;i++)
- {
- scanf("%d%d%d",&u,&v,&w);
- c[u][v]+=w;
- }
- s=1;t=n;
- maxflow=0;
- while(flow=dfs(s,inf))
- {
- maxflow+=flow;
- memset(vis,0,sizeof(vis));
- }
- printf("Case %d: %d\n",k++,maxflow);
- }
- return 0;
- }
- #include<stdio.h>
- #include<string.h>
- #define maxn 16
- #define inf 9999999
- int c[maxn][maxn];
- int s,t,i,k,u,v,w,n,m;
- int flow,maxflow;
- int vis[maxn];
- int dfs(int u,int low)
- {
- int i,flow;
- int sum=0;
- if(u==t)
- return low;
- if(vis[u])
- return 0;
- vis[u]=1;
- for(i=1;i<=n;i++)
- if(c[u][i]&&(flow=dfs(i,low<c[u][i]?low:c[u][i])))
- {
- sum+=flow;
- low-=flow;
- c[u][i]-=flow;
- c[i][u]+=flow;
- if(!low)
- break;
- }
- return sum;
- }
- int main()
- {
- int ca,k=1;
- scanf("%d",&ca);
- while(ca--)
- {
- memset(vis,0,sizeof(vis));
- memset(c,0,sizeof(c));
- scanf("%d%d",&n,&m);
- for(i=1;i<=m;i++)
- {
- scanf("%d%d%d",&u,&v,&w);
- c[u][v]+=w;
- }
- s=1;t=n;
- maxflow=0;
- while(flow=dfs(s,inf))
- {
- maxflow+=flow;
- memset(vis,0,sizeof(vis));
- }
- printf("Case %d: %d\n",k++,maxflow);
- }
- return 0;
- }
╝②
§2.2Edmonds-Karp(EK)算法實現
Edmonds-Karp是一種求網絡最大流的算法,最爲Ford-Fulkson類算法中最簡單的一個算法,與Ford-Fulkerson算法不同的是Edmonds-Karp要求每次找長度最短的增廣路徑,即使用BFS查找增廣路徑。
時間複雜度是O(n·m²),空間複雜度是O(n²)。
- void ford_fulkerson(int s,int e)
- {
- queue<int>Q;
- int u,v;
- while(1)
- {
- Maxflow=0;//最大流初始化
- memset(maxflow,0,sizeof(maxflow));//每次尋找增廣路徑都將每個點的流入容量置爲0
- memset(visit,0,sizeof(visit));//標記一個點是否已經壓入隊列
- maxflow[s]=INT_MAX;//源點的容量置爲正無窮
- Q.push(s); // 將源點壓入隊列
- while(!Q.empty()) //當隊列不爲空
- {
- u=Q.front();
- Q.pop();
- for(v=1;v<=N;v++)
- {
- if(!visit[v]&&flow[u][v]>0)
- {
- visit[v]=1;
- father[v]=u;//記錄下他的父親方便往後的正反向更新
- Q.push(v);
- maxflow[v]=(maxflow[u]<flow[u][v]?maxflow[u]:flow[u][v]);//當前點的容量爲父親點容量與邊流量的較小者
- }
- }
- if(maxflow[e]>0) //如果找到了匯點並且匯點容量不爲0則清空隊列。
- {
- while(!Q.empty())
- Q.pop();
- break;
- }
- }
- if(maxflow[e]==0)//已經找不到到匯點的增光路經了,就退出整個循環
- break;
- for(i=e;i!=s;i=father[i])
- {
- flow[father[i]][i]-=maxflow[e];//正向更新
- flow[i][father[i]]+=maxflow[e];//反向更新
- }
- Maxflow+=maxflow[e];//更新最大流
- }
- }
§2.3Dinic算法
Dinic 算法是其中一種比較高效的方法,是EK算法的改進,減少查找增廣路徑次數,其複雜度爲 O(V²*E)。
Dinic 算法的步驟
1) 計算殘餘網絡的層次圖。我們定義 h 爲頂點 i 距離源 S 所經過到最小邊數,求出所有頂點的 h 值,h[] 值相同的頂點屬於同一層,這就是網絡的層次圖。
2) 在層次圖上進行 BFS 增廣,直到不存在增廣路徑。這時求得的增廣路徑上頂點是分層的,路徑上不可能存在兩個頂點屬於同一層,即 h== h[j] (i!= j )。同時,求得層次圖後,我們可以在層次圖上進行多次增廣。
3) 重複 1 和 2。直到不存在增廣路徑。
可知,Dinic 算法找到的增廣路徑是最短的,即經過的頂點數最少。再者,Dinic 算法找一條增廣路徑同時可以找到多條,類似增廣路徑樹。比如我們找到了一條增廣路徑,這條增廣路徑所增加的流量爲 C,則這條增廣路徑上必然有一條邊<i,j>殘餘容量爲 C,這是我們不必又從起點開始尋找增廣路,而是從 i 頂點出發找增廣路,這樣就減少了重複計算,提高了效率。
Dinic算法實現
╔
- //--------Dinic的非遞歸正向實現---------
- #define NOT(x) (x&1?x+1:x-1)
- struct Edge
- {
- int u;
- int value;
- int next;
- }edge[MAXM*2];
- int level[MAXN],queue[MAXN],node[MAXN],variednode[MAXN];//level給每一個節點分級,即分層;queue分層時當隊列用,找增廣路時
- int front,rear;//當棧用;node[i]爲結點i的指針;variednode可變的結點指針(原本是node的複製,但是他不斷變化);front,rear
- int index;//即隊列的首尾;top即棧頂;index即作爲edge的下標;
- int top;
- void Build_Graph(int m)//建圖即建立網絡
- {
- int v,u,value;
- index=0; memset(node,-1,sizeof(node));
- for(int i=0;i<m;i++)
- {
- scanf("%d %d %d",&v,&u,&value);
- ++index; //這裏用數組模擬指針,模擬鏈表結構
- edge[index].u=u; //重在自己慢慢體會,講不出來效果的……
- edge[index].value=value;
- edge[index].next=node[v];
- node[v]=index;/////建立原網絡
- ++index;
- edge[index].u=v;
- edge[index].value=0;
- edge[index].next=node[u];
- node[u]=index;/////建立反網絡
- }
- }
- int Dinic(int source,int sink,int n)
- {
- int maxflow=0;
- int v,u,value;
- while(true)
- {
- memset(level,0,sizeof(level));
- front=rear=0; level[source]=1;
- queue[0]=source;
- while(front<=rear)/////傳說中的分層
- {
- v=queue[front++];//注意這裏的queue當隊列用儲存的是結點
- for(int i=node[v];i!=-1;i=edge[i].next)
- {
- u=edge[i].u; value=edge[i].value;
- if(value && level[u]==0)
- {
- level[u]=level[v]+1;
- queue[++rear]=u;
- if(u==sink) break;
- }
- }
- if(u==sink) break;
- }
- if(level[sink]==0) break;//這個就是判斷是否存在增廣路,沒有就結束了
- for(int i=1;i<=n;i++) variednode[i]=node[i];//看variednode——node的複製,以後就不斷變化。這就是優化,記錄下次開始訪問的邊。
- edge[0].u=source; top=0; queue[++top]=0;//這裏的所做是爲了“湊”下邊的循環,這裏的queue做棧用儲存邊的下標
- while(top)//////求該分層下的最短增廣路
- {
- int i; v=edge[queue[top]].u;
- for(i=variednode[v];i!=-1;i=edge[i].next)
- {
- u=edge[i].u; value=edge[i].value;
- if(value && level[u]==level[v]+1)
- {
- queue[++top]=i;
- variednode[v]=edge[i].next;
- break;
- }
- }
- if(i==-1) { variednode[v]=-1;top--;continue; }//若該點四周不存在最短增廣路,則直接variednode[v]=-1;以防下次做
- //多餘的查找。top--退一條邊,再找
- if(u==sink)//找到一條邊就判斷一下該點是不是sink,不是繼續向下找
- {
- int min=0x7fffffff,flag;
- for(i=2;i<=top;i++)
- if(min>edge[i].value)
- min=edge[i].value;//找到最大流量
- for(i=top;i>1;i--)
- {
- if(min==edge[queue[i]].value)
- flag=i;//更新該正向路徑的各個流量,並標記出第一個流量變爲0的邊所在的棧位
- edge[queue[i]].value-=min;
- edge[NOT(queue[i])].value+=min;//看看怎麼建圖的吧 }
- top=flag-1;//更新top
- maxflow+=min;//更新maxflow
- }
- }
- }
- return maxflow;
- }
╝④
§2.4SAP算法(最短路徑增廣算法)
SAP算法計算的反向層次圖即從當前頂點到匯點t的距離,而Dinic算法是從源點s到當前頂點的距離。
SAP算法流程
1、定義距離標號爲各點到匯點距離的下界(即最短距離)。
2、在初始距離標號的基礎上,不斷沿着可行弧找增廣路。可行弧的定義爲{( i , j ) , h[ i ]==h[ j ]+1 };
3、遍歷當前節點完以後,爲了保證下次再來的時候有路可走,重新標號當前距離。
h[ i ] = min(h[ j ] +1);
4、檢查重新標記的頂點,若其爲原點,且被標記的高度等於節點個數時,圖中已經不存在增廣路,算法可結束。否則繼續從原點開始遍歷
SAP算法實現
- #include <iostream>
- #include <cstring>
- #include <cstdlib>
- usingnamespace std;
- constint Max =225;
- constint oo =210000000;
- int n,m,c[Max][Max],r[Max][Max],c1[Max][Max],source,sink;
- //c1是c的反向網絡,用於dis數組的初始化
- int dis[Max];
- void initialize()// 初始化dis數組
- {
- int q[Max],head =0, tail =0;//BFS隊列
- memset(dis,0,sizeof(dis));
- q[++head] = sink;
- while(tail < head)
- {
- int u = q[++tail], v;
- for(v =0; v <= sink; v++)
- {
- if(!dis[v] && c1[u][v] >0)
- {
- dis[v] = dis[u] +1;
- q[++head] = v;
- }
- }
- }
- }
- int maxflow_sap()
- {
- initialize();
- int top = source, pre[Max], flow =0, i, j, k, low[Max];
- // top是當前增廣路中最前面一個點。
- memset(low,0,sizeof(low));//low數組用於保存路徑中的最小容量
- while(dis[source] < n)
- {
- bool flag =false;
- low[source] = oo;
- for(i =0; i <= sink; i++)//找允許弧,根據允許弧的定義
- {
- if(r[top][i] >0&& dis[top] == dis[i] +1)
- {
- flag =true;
- break;
- }
- }
- if(flag)// 如果找到允許弧
- {
- low[i] = r[top][i];
- if(low[i] > low[top]) low[i] = low[top];//更新low
- pre[i] = top; top = i;
- if(top == sink)// 如果找到一條增廣路了
- {
- flow += low[sink];
- j = top;
- while(j != source)// 路徑回溯更新殘留網絡
- {
- k = pre[j];
- r[k][j] -= low[sink];
- r[j][k] += low[sink];
- j = k;
- }
- top = source;//從頭開始再找最短路
- memset(low,0,sizeof(low));
- }
- }
- else// 如果沒有允許弧
- {
- int mindis =10000000;
- for(j =0; j <= sink; j++)//找和top相鄰dis最小的點
- {
- if(r[top][j] >0&& mindis > dis[j] +1)
- mindis = dis[j] +1;
- }
- dis[top] = mindis;//更新top的距離值
- if(top != source) top = pre[top];// 回溯找另外的路
- }
- }
- return(flow);
- }
SAP算法使用GAP優化實現
- /*
- sap+gap優化,採用鄰接矩正
- 時間複雜度:O(mn^2)
- */
- #include<iostream>
- #include<queue>
- using namespace std;
- const int msize=205; //最大頂點數
- const int inf=0x7fffffff;
- int dis[msize]; //標號
- int r[msize][msize]; //殘留網絡,初始爲原圖
- int num[msize]; //num[i]表示標號爲i的頂點數有多少
- int pre[msize];
- int n,m,src,des; //n個頂點,m條邊,源點src,匯點des
- void rev_bfs() //反向bfs計算標號,匯點des標號爲0
- {
- int i,k;
- for(i=1;i<=n;i++)
- {
- dis[i]=n;
- num[i]=0;
- }
- queue<int> q;
- q.push(des);
- dis[des]=0;
- num[0]=1;
- while(!q.empty())
- {
- k=q.front();
- q.pop();
- for(i=1;i<=n;i++)
- {
- if(dis[i]==n&&r[i][k]>0)
- {
- dis[i]=dis[k]+1;
- q.push(i);
- num[dis[i]]++;
- }
- }
- }
- }
- int findArc(int i)
- {
- for(int j=1;j<=n;j++)
- {
- if(r[i][j]>0&&dis[i]==dis[j]+1)
- return j;
- }
- return -1;
- }
- int reLable(int i)
- {
- int mindis=n;
- for(int j=1;j<=n;j++)
- {
- if(r[i][j]>0)
- mindis=mindis<dis[j]? mindis:dis[j];
- }
- return mindis;
- }
- int maxFlow()
- {
- rev_bfs();
- int totalflow=0, i=src, j, k;
- int d; //增量
- pre[src]=-1;
- while(dis[src]<n)
- {
- j=findArc(i);
- if(j>=0)
- {
- pre[j]=i;
- i=j;
- if(i==des)
- {
- d=inf;
- for(k=des;k!=src;k=pre[k])
- d=d<r[pre[k]][k]? d:r[pre[k]][k];
- for(k=des;k!=src;k=pre[k])
- {
- r[pre[k]][k]-=d;
- r[k][pre[k]]+=d;
- }
- totalflow+=d;
- i=src;
- }
- }
- else
- {
- --num[dis[i]];
- if(0==num[dis[i]]) return totalflow;
- int x=reLable(i);
- dis[i]=x+1;
- num[dis[i]]++;
- if(i!=src) i=pre[i];
- }
- }
- return totalflow;
- }
- int main()
- {
- while(scanf("%d%d",&m,&n)!=EOF)
- {
- int i;
- int u,v,w;
- memset(r,0,sizeof(r));
- for(i=0;i<m;i++)
- {
- scanf("%d%d%d",&u,&v,&w);
- r[u][v]+=w;
- }
- src=1;
- des=n;
- printf("%d\n",maxFlow());
- }
- return 0;
- }
§2.5Preflow push method(預流-推進最大流方法)
在一個流網絡中,預流(preflow)是一個正邊流的集合,滿足一下條件:每條邊的流不大於該邊的容量,且對於每個內部結點,其上的流入量不小於它的流出量。活動頂點是一個流入量大於流出量的內部結點。把活動頂點v的流入量和流出量的差稱爲該頂點的盈餘量(excess)e(v)。
最大推進:如果一個頂點v對每一個鄰點u都以容量大小(c<v,u>)的流向前推進過,就說頂點v達到最大推進。
Preflow push method的原理
Preflow push method是給源點一個足夠大(等於源點鄰接邊的容量之和)的流,向鄰近結點推進(push),得到了一個活動頂點(還沒有流出,只是流入)集合,然後將這個集合的點沿着路徑推進(流出)不斷的減小盈餘量(或者是回退給上一個頂點),直到沒有活動頂點爲止。每次推進的流量是邊的容量。
增大路徑算法總是維持一個可行流(流出量等於流入量):它沿着增大路徑增加流,最終得到最大流。而預流-推進算法維持的最大流不是可行流,因爲某些頂點的流入量比流出量要大:它們通過這些頂點推進(push)流,直到達到一個可行流。再用水流來說明兩者的區別:增大路徑算法相當於查找一個所有河流,獲得每條河流的最大流量(其中流量最小作爲一條河流的最大流量,否則會出現決堤),最後把每條河流的最大流量累加就是最大流;預流-推進方法相當於水庫放水的過程,即最大流相當於從水庫以河道的最大容量向下遊推進的過程。
Preflow push 方法的流程
1.給源點s一個足夠(等於源點鄰接邊的容量之和)的流,流向它的鄰近頂點,使它的鄰近頂點構成一個活動頂點的集合A
2.遍歷活動頂點集合A的每一個點v,對v進行如下操作:
(1)對v的每一條鄰接邊以邊容量的流向前推進流(如果每條鄰邊都以邊容量推進過的(達到最大推進),則執行(2)),並將該頂點加入集合A
(2)若(1)操作後頂點v的還有盈餘量(e(v)>0),則將流量退回(逆推進)給上一個頂點(流給它的頂點),最後將v從集合A中移除,其中退回給的上一個頂點就變成了活動頂點要進入集合A
3.直到活動頂點集合A爲空爲止,流向匯點t的流量就是最大流量。
退回操作釋疑
這裏主要的難點就是爲什麼要做退回(逆推進)的操作。用一個例子來說明即可:對於頂點v,它有A和B兩條路徑到達匯點t,v的流入量是5,向A路徑的第一個頂點a推進的流量是4(c<v,a>=4),那麼只能向B路徑的一個頂點b推進的流量是1(這時c<v,b>=3),但是在A路徑最終只有2流向匯點t,出現了盈餘量,在B路徑去沒有達到最大流量,那麼應該把A路徑的盈餘量轉移到B路徑去推進,這得靠退回(逆推進)操作來完成。退回操作就是把盈餘量逆推進給上一個頂點
Push relabel 算法(壓入與重標記方法)
Preflow push方法是將活動頂點沿着鄰接邊以邊的容量(c<v,u>)爲最大流量向前推進流,以源點s進行BFS分層得到的樹結構,同一層(到源點s路徑長度相等)的頂點的地位是一樣的,同一層的每個頂點都可以得到最大盈餘量(c<v,u>),如果盈餘量大於0,則要做退回操作。Push relabel算法的原理是以高度來表示由源點s向前的層次,源點s層次最高,每次推進操作是從高頂點往低頂點壓入,如果頂點v還有盈餘量(鄰近頂點u高度大於等於v的高度,或者有剩餘)則增加高度(就是Preflow push的退回操作)。
Push Relabel算法的流程
1)、構造初始流。將源點高度標記爲n,其餘點高度均爲0;
2)、如果殘留網絡中不存在活結點,則算法結束。否則繼續執行下一步;
3)、選取活結點,如果該結點出邊爲可推流邊,則沿此邊推流,否則將該節點高度加1,執行步驟2)。
壓入與重標記算法實現
╔
- struct Node{
- int v;
- Node *next;
- }
- Node g[MAX]; //用鄰接表存儲
- int resi[MAX][MAX]; //殘留容量
- int e[MAX],h[MAX]; //頂點的餘流和高度
- int Push_Relabel(int s,int t,int n){
- queue<int> que;
- int i,u,_min,sum=0;
- Node *p;
- //初始化頂點高度和餘流
- memset(e,0,sizeof(e));
- memset(h,0,sizeof(h));
- h[s]=n;
- e[0]=(1<<30);
- que.push(s); //將源點進隊
- while(!que.empty()){
- u=que.front();
- que.pop();
- for(p=g[u].next;p;p=p->next){
- _min=resi[u][p->v]<e[u]?resi[u][p->v]:e[u]; //取頂點餘流和相鄰邊的殘留容量的最小值
- //如果h[u]<=h[p->v],則應執行中標記操作;如果h[u]==h[p->v]+1,則執行壓入操作
- if(_min&&(h[u]==h[p->v]+1 || u==s)){
- resi[u][p->v]-=_min;
- resi[p->v][u]+=_min;
- e[u]-=_min;
- e[p->v]+=_min;
- if(p->v==t)sum+=_min; //如果到達了匯點,就將流值加入到最大流中
- else if(p->v!=s)que.push(p->v); //只有既不是源點也不是匯點才進隊
- }
- }
- //如果不是源點且仍有餘流,則重標記高度再進隊。
- //這裏只是簡單的將高度增加了一個單位,也可以向上面所說的一樣賦值爲最低的相鄰頂點的高度高一個單位
- if(u!=s&&e[u]){
- h[u]++;
- que.push(u);
- }
- }
- return sum;
- }
╝③
疑問:
個人一直都有一個疑問就是Push relabel算法怎麼沒有進行退回操作,只是把高度增加(貌似只是爲了推進),並沒有把盈餘量逆推進給指向它的頂點,還是沒有解決上面退回釋疑的情況?
Relabel to front 算法(重標記與前移算法)
Relabel-to-Front使用一個鏈表保存盈餘頂點(盈餘量>0),用Discharge操作不斷使盈餘頂點不再溢出。Discharge的操作過程是:若找不到可被壓入的臨邊,則重標記,否則對臨邊壓入,直至點不再盈餘。算法的主過程是:首先將源點出發的所有邊充滿,然後將除源和匯外的所有頂點保存在一個鏈表裏,從鏈表頭開始進行Discharge,如果完成後頂點的高度有所增加,則將這個頂點置於鏈表的頭部,對下一個頂點開始Discharge。
Relabel-to-Front算法的時間複雜度是O(V^3),還有一個叫Highest Label Preflow Push的算法複雜度據說是O(V^2*E^0.5)。我研究了一下HLPP,感覺它和Relabel-to-Front本質上沒有區別,因爲Relabel-to-Front每次前移的都是高度最高的頂點,所以也相當於每次選擇最高的標號進行更新。還有一個感覺也會很好實現的算法是使用隊列維護盈餘頂點,每次對pop出來的頂點discharge,出現了新的盈餘頂點時入隊。
最高標號預流推進算法(暫時擱淺)
§3最小費用最大流問題
最小費用最大流問題求解算法有
1、 連續最短路算法(Successive Shortest Path);
2、 消圈算法(Cycle Canceling);
3、 原始對偶算法(Primal Dual);
4、 網絡單純形(Network Simplex)。
§4有向圖強連通分量的問題
有向圖的強連通分支算法(strongly connected component),該算法是圖深度優先搜索算法的另一重要應用。強分支算法可以將一個大圖分解成多個連通分支,某些有向圖算法可以分別在各個聯通分支上獨立運行,最後再根據分支之間的關係將所有的解組合起來。
在無向圖中,如果頂點s到t有一條路徑,則可以知道從t到s也有一條路徑;在有向無環圖中個,如果頂點s到t有一條有向路徑,則可以知道從t到s必定沒有一條有向路徑;對於一般有向圖,如果頂點s到t有一條有向路徑,但是無法確定從t到s是否有一條有向路徑。可以藉助強連通分支來研究一般有向圖中頂點之間的互達性。
有向圖G=(V, E)的一個強連通分支就是一個最大的頂點子集C,對於C中每對頂點(s, t),有s和t是強連通的,並且t和 s也是強連通的,即頂點s和t是互達的。圖中給出了強連通分支的例子。我們將分別討論3種有向圖中尋找強連通分支的算法。
3種算法分別爲Kosaraju算法、Tarjan算法和Gabow算法,它們都可以在線性時間內找到圖的強連通分支。
╔
一、 Kosaraju算法
1. 算法思路
基本思路:
這個算法可以說是最容易理解,最通用的算法,其比較關鍵的部分是同時應用了原圖G和反圖GT。(步驟1)先用對原圖G進行深搜形成森林(樹),(步驟2)然後任選一棵樹對其進行深搜(注意這次深搜節點A能往子節點B走的要求是EAB存在於反圖GT),能遍歷到的頂點就是一個強連通分量。餘下部分和原來的森林一起組成一個新的森林,繼續步驟2直到 沒有頂點爲止。
改進思路:
當然,基本思路實現起來是比較麻煩的(因爲步驟2每次對一棵樹進行深搜時,可能深搜到其他樹上去,這是不允許的,強連通分量只能存在單棵樹中(由開篇第一句話可知)),我們當然不這麼做,我們可以巧妙的選擇第二深搜選擇的樹的順序,使其不可能深搜到其他樹上去。想象一下,如果步驟2是從森林裏選擇樹,那麼哪個樹是不連通(對於GT來說)到其他樹上的呢?就是最後遍歷出來的樹,它的根節點在步驟1的遍歷中離開時間最晚,而且可知它也是該樹中離開時間最晚的那個節點。這給我們提供了很好的選擇,在第一次深搜遍歷時,記錄時間i離開的頂點j,即numb[i]=j。那麼,我們每次只需找到沒有找過的頂點中具有最晚離開時間的頂點直接深搜(對於GT來說)就可以了。每次深搜都得到一個強連通分量。
隱藏性質:
分 析到這裏,我們已經知道怎麼求強連通分量了。但是,大家有沒有注意到我們在第二次深搜選擇樹的順序有一個特點呢?如果在看上述思路的時候,你的腦子在思 考,相信你已經知道了!!!它就是:如果我們把求出來的每個強連通分量收縮成一個點,並且用求出每個強連通分量的順序來標記收縮後的節點,那麼這個順序其 實就是強連通分量收縮成點後形成的有向無環圖的拓撲序列。爲什麼呢?首先,應該明確搜索後的圖一定是有向無環圖呢?廢話,如果還有環,那麼環上的頂點對應 的所有原來圖上的頂點構成一個強連通分量,而不是構成環上那麼多點對應的獨自的強連通分量了。然後就是爲什麼是拓撲序列,我們在改進分析的時候,不是先選 的樹不會連通到其他樹上(對於反圖GT來說),也就是後選的樹沒有連通到先選的樹,也即先出現的強連通分量收縮的點只能指向後出現的強連通分量收縮的點。那麼拓撲序列不是理所當然的嗎?這就是Kosaraju算法的一個隱藏性質。
2. 僞代碼
Kosaraju_Algorithm:
step1:對原圖G進行深度優先遍歷,記錄每個節點的離開時間。
step2:選擇具有最晚離開時間的頂點,對反圖GT進行遍歷,刪除能夠遍歷到的頂點,這些頂點構成一個強連通分量。
step3:如果還有頂點沒有刪除,繼續step2,否則算法結束。
3. 實現代碼:
#include <iostream>
using namespace std;
const int MAXN = 110;
typedef int AdjTable[MAXN]; //鄰接表類型
int n;
bool flag[MAXN]; //訪問標誌數組
int belg[MAXN]; //存儲強連通分量,其中belg[i]表示頂點i屬於第belg[i]個強連通分量
int numb[MAXN]; //結束時間標記,其中numb[i]表示離開時間爲i的頂點
AdjTable adj[MAXN], radj[MAXN]; //鄰接表,逆鄰接表
//用於第一次深搜,求得numb[1..n]的值
void VisitOne(int cur, int &sig)
{
flag[cur] = true;
for ( int i=1; i<=adj[cur][0]; ++i )
{
if ( false==flag[adj[cur][i]] )
{
VisitOne(adj[cur][i],sig);
}
}
numb[++sig] = cur;
}
//用於第二次深搜,求得belg[1..n]的值
void VisitTwo(int cur, int sig)
{
flag[cur] = true;
belg[cur] = sig;
for ( int i=1; i<=radj[cur][0]; ++i )
{
if ( false==flag[radj[cur][i]] )
{
VisitTwo(radj[cur][i],sig);
}
}
}
//Kosaraju算法,返回爲強連通分量個數
int Kosaraju_StronglyConnectedComponent()
{
int i, sig;
//第一次深搜
memset(flag+1,0,sizeof(bool)*n);
for ( sig=0,i=1; i<=n; ++i )
{
if ( false==flag[i] )
{
VisitOne(i,sig);
}
}
//第二次深搜
memset(flag+1,0,sizeof(bool)*n);
for ( sig=0,i=n; i>0; --i )
{
if ( false==flag[numb[i]] )
{
VisitTwo(numb[i],++sig);
}
}
return sig;
}
二、 Trajan算法
1. 算法思路:
這 個算法思路不難理解,由開篇第一句話可知,任何一個強連通分量,必定是對原圖的深度優先搜索樹的子樹。那麼其實,我們只要確定每個強連通分量的子樹的根, 然後根據這些根從樹的最低層開始,一個一個的拿出強連通分量即可。那麼身下的問題就只剩下如何確定強連通分量的根和如何從最低層開始拿出強連通分量了。
那麼如何確定強連通分量的根,在這裏我們維護兩個數組,一個是indx[1..n],一個是mlik[1..n],其中indx[i]表示頂點i開始訪問時間,mlik[i]爲與頂點i鄰接的頂點未刪除頂點j的mlik[j]和mlik[i]的最小值(mlik[i]初始化爲indx[i])。這樣,在一次深搜的回溯過程中,如果發現mlik[i]==indx[i]那麼,當前頂點就是一個強連通分量的根,爲什麼呢?因爲如果它不是強連通分量的跟,那麼它一定是屬於另一個強連通分量,而且它的根是當前頂點的祖宗,那麼存在包含當前頂點的到其祖宗的迴路,可知mlik[i]一定被更改爲一個比indx[i]更小的值。
至於如何拿出強連通分量,這個其實很簡單,如果當前節點爲一個強連通分量的根,那麼它的強連通分量一定是以該根爲根節點的(剩下節點)子 樹。在深度優先遍歷的時候維護一個堆棧,每次訪問一個新節點,就壓入堆棧。現在知道如何拿出了強連通分量了吧?是的,因爲這個強連通分量時最先被壓人堆棧 的,那麼當前節點以後壓入堆棧的並且仍在堆棧中的節點都屬於這個強連通分量。當然有人會問真的嗎?假設在當前節點壓入堆棧以後壓入並且還存在,同時它不屬 於該強連通分量,那麼它一定屬於另一個強連通分量,但當前節點是它的根的祖宗,那麼這個強連通分量應該在此之前已經被拿出。現在沒有疑問了吧,那麼算法介 紹就完了。
2. 僞代碼:
Tarjan_Algorithm:
step1:
找一個沒有被訪問過的節點v,goto step2(v)。否則,算法結束。
step2(v):
初始化indx[v]和mlik[v]
對於v所有的鄰接頂點u:
1) 如果沒有訪問過,則step2(u),同時維護mlik[v]
2) 如果訪問過,但沒有刪除,維護mlik[v]
如果indx[v]==mlik[v],那麼輸出相應的強連通分量
3. 實現代碼
#include <iostream>
using namespace std;
const int MAXN = 110;
const char NOTVIS = 0x00; //頂點沒有訪問過的狀態
const char VIS = 0x01; //頂點訪問過,但沒有刪除的狀態
const char OVER = 0x02; //頂點刪除的狀態
typedef int AdjTable[MAXN]; //鄰接表類型
int n;
char flag[MAXN]; //用於標記頂點狀態,狀態有NOTVIS,VIS,OVER
int belg[MAXN]; //存儲強連通分量,其中belg[i]表示頂點i屬於第belg[i]個強連通分量
int stck[MAXN]; //堆棧,輔助作用
int mlik[MAXN]; //很關鍵,與其鄰接但未刪除頂點地最小訪問時間
int indx[MAXN]; //頂點訪問時間
AdjTable adj[MAXN]; //鄰接表
//深搜過程,該算法的主體都在這裏
void Visit(int cur, int &sig, int &scc_num)
{
int i;
stck[++stck[0]] = cur; flag[cur] = VIS;
mlik[cur] = indx[cur] = ++sig;
for ( i=1; i<=adj[cur][0]; ++i )
{
if ( NOTVIS==flag[adj[cur][i]] )
{
Visit(adj[cur][i],sig,scc_num);
if ( mlik[cur]>mlik[adj[cur][i]] )
{
mlik[cur] = mlik[adj[cur][i]];
}
}
else if ( VIS==flag[adj[cur][i]] )
{
if ( mlik[cur]>indx[adj[cur][i]] ) //該部分的indx應該是mlik,但是根據算法的屬性,使用indx也可以,且時間更少
{
mlik[cur] = indx[adj[cur][i]];
}
}
}
if ( mlik[cur]==indx[cur] )
{
++ scc_num;
do
{
belg[stck[stck[0]]] = scc_num;
flag[stck[stck[0]]] = OVER;
}
while ( stck[stck[0]--]!=cur );
}
}
//Tarjan算法,求解belg[1..n],且返回強連通分量個數,
int Tarjan_StronglyConnectedComponent()
{
int i, sig, scc_num;
memset(flag+1,NOTVIS,sizeof(char)*n);
sig = 0; scc_num = 0; stck[0] = 0;
for ( i=1; i<=n; ++i )
{
if ( NOTVIS==flag[i] )
{
Visit(i,sig,scc_num);
}
}
return scc_num;
}
三、 Gabow算法
1. 思路分析
這個算法其實就是Tarjan算法的變異體,我們觀察一下,只是它用第二個堆棧來輔助求出強連通分量的根,而不是Tarjan算法裏面的indx[]和mlik[]數組。那麼,我們說一下如何使用第二個堆棧來輔助求出強連通分量的根。
我們使用類比方法,在Tarjan算法中,每次mlik[i]的修改都是由於環的出現(不然,mlik[i]的值不可能變小),每次出現環,在這個環裏面只剩下一個mlik[i]沒有被改變(深度最低的那個),或者全部被改變,因爲那個深度最低的節點在另一個環內。那麼Gabow算 法中的第二堆棧變化就是刪除構成環的節點,只剩深度最低的節點,或者全部刪除,這個過程是通過出棧來實現,因爲深度最低的那個頂點一定比前面的先訪問,那 麼只要出棧一直到棧頂那個頂點的訪問時間不大於深度最低的那個頂點。其中每個被彈出的節點屬於同一個強連通分量。那有人會問:爲什麼彈出的都是同一個強連 通分量?因爲在這個節點訪問之前,能夠構成強連通分量的那些節點已經被彈出了,這個對Tarjan算法有了解的都應該清楚,那麼Tarjan算法中的判斷根我們用什麼來代替呢?想想,其實就是看看第二個堆棧的頂元素是不是當前頂點就可以了。
現在,你應該明白其實Tarjan算法和Gabow算法其實是同一個思想的不同實現,但是,Gabow算法更精妙,時間更少(不用頻繁更新mlik[])。
2. 僞代碼
Gabow_Algorithm:
step1:
找一個沒有被訪問過的節點v,goto step2(v)。否則,算法結束。
step2(v):
將v壓入堆棧stk1[]和stk2[]
對於v所有的鄰接頂點u:
1) 如果沒有訪問過,則step2(u)
2) 如果訪問過,但沒有刪除,維護stk2[](處理環的過程)
如果stk2[]的頂元素==v,那麼輸出相應的強連通分量
3. 實現代碼
#include <iostream>
using namespace std;
const int MAXN = 110;
typedef int AdjTable[MAXN]; //鄰接表類型
int n;
int intm[MAXN]; //標記進入頂點時間
int belg[MAXN]; //存儲強連通分量,其中belg[i]表示頂點i屬於第belg[i]個強連通分量
int stk1[MAXN]; //輔助堆棧
int stk2[MAXN]; //輔助堆棧
AdjTable adj[MAXN]; //鄰接表
//深搜過程,該算法的主體都在這裏
void Visit(int cur, int &sig, int &scc_num)
{
int i;
intm[cur] = ++sig;
stk1[++stk1[0]] = cur;
stk2[++stk2[0]] = cur;
for ( i=1; i<=adj[cur][0]; ++i )
{
if ( 0==intm[adj[cur][i]] )
{
Visit(adj[cur][i],sig,scc_num);
}
else if ( 0==belg[adj[cur][i]] )
{
while ( intm[stk2[stk2[0]]]>intm[adj[cur][i]] )
{
-- stk2[0];
}
}
}
if ( stk2[stk2[0]]==cur )
{
-- stk2[0]; ++ scc_num;
do
{
belg[stk1[stk1[0]]] = scc_num;
}
while ( stk1[stk1[0]--]!=cur );
}
}
//Gabow算法,求解belg[1..n],且返回強連通分量個數,
int Gabow_StronglyConnectedComponent()
{
int i, sig, scc_num;
memset(belg+1,0,sizeof(int)*n);
memset(intm+1,0,sizeof(int)*n);
sig = 0; scc_num = 0; stk1[0] = 0; stk2[0] = 0;
for ( i=1; i<=n; ++i )
{
if ( 0==intm[i] )
{
Visit(i,sig,scc_num);
}
}
return scc_num;
}
寫到這裏,做一個總結:Kosaraju算法的第二次深搜隱藏了一個拓撲性質,而Tarjan算法和Gabow算法省略了第二次深搜,所以,它們不具有拓撲性質。Tarjan算法用堆棧和標記,Gabow用兩個堆棧(其中一個堆棧的實質是代替了Tarjan算法的標記部分)來代替Kosaraju算法的第二次深搜,所以只用一次深搜,效率比Kosaraju算法高。
╝⑤
§5問題歸約
╔
最大流歸約
1.允許多源點和多匯點或增加頂點容量約束的流網絡,其最大流問題等價於標準最大流問題(前面講解的情況就是標準最大流問題)。
如對於頂點容量約束我們可以用頂點u和u*對於原來頂點u,邊<u,u*>的容量就是頂點u的容量。
2.無環網的最大流問題等價於標準最大流問題。
無環網改造成多源點和多匯點最大流問題。
3.可行流問題可歸約到最大流問題。
可行流
假設在一個流網絡中爲每一個頂點賦予一個全職,並將此解釋爲供應(正)或需求(負),而且頂點權值之和爲0,可行流可以定義爲每個頂點的流出量與流入量之差等於那個頂點的權值。給定一個這樣的網,確定是否存在一個可行流。
4.最大基數二分匹配問題可以歸約到最大流的可行流。
最大基數二分匹配
給定一個二分圖,找出最大基數的一個邊集,滿足每個頂點至多連接到一個其他頂點。
5.二分圖問題可歸約到最大流問題。
給定一個二分圖問題,通過爲所有邊指定從一個集合到另一集合的方向,並添加一個源點以及該源點指向二分圖中其中一個集合的所有頂點的邊,在如此添加一個匯點到另一個集合。每條邊的容量爲1。
6.(Menger定理)在有向圖中刪除某條邊是兩個頂點不連通的最少邊等於這兩個頂點之間邊不相交的路徑的最大數目。
給定一個有向圖,用同一組頂點和邊定義一個網絡,其中所有邊的容量均定義爲1,任何一個從s到t的邊不相交的路徑數目等於流值。
最小成本流歸約
1.(無負環的)單源點最短路徑問題可歸約爲最小成本可行流問題。
2.分配問題,郵差問題,運輸問題等價於最小成本流問題。
╝⑥
§6小結
本文粗略地介紹了有向強連通求解和網絡流大講堂——求解最大流(最小割)、最小費用最大流的相關知識,雖然還沒有最終完工,由於一直放不下心總想完成,但又苦於沒有大量時間去學習,就先暫且擱淺(草草結束這篇文章的相關理論的整理和寫作),日後有閒暇定當完成。如果你有任何建議或者批評和補充,請留言指出,不勝感激,更多參考請移步互聯網。
參考
①plussai:http://plussai.iteye.com/blog/1128127
②'wind:http://www.cnblogs.com/dream-wind/archive/2012/03/15/2397192.html
③
http://chhaj5236.blog.163.com/blog/static/11288108120099725027512/
④_Never_:http://www.cnblogs.com/fornever/archive/2011/09/20/2182903.html
⑤http://www.cppblog.com/koson/archive/2010/04/27/113694.html
⑥Robert Sedgewick: Algorithm in C