開始總覺得網絡流是多麼高深的東西,一直不敢去接受,然而學完之後發現好像也不是太難哦,只是好多基礎東西的一些整合。ios
文章中可能會有多出紕漏,敬請讀者不吝賜教。 算法
咱們以一個經典的問題引入算法。數組
你所在的村莊新開通了地下流水管道,自來水廠源源不斷的提供水,村民們用水直接或間接用水,而村莊用完的廢水統一回收於另外一點(設排來的水所有回收)。固然每一個管道有必定的容量,廢水站求出最多能夠匯聚多少水?網絡
固然這是一個有向圖。ide
容量:每條邊都有一個容量(水管的最大水流容量)優化
源點:出發點(水廠)。spa
匯點:結束點(廢水站)。指針
流:一個合法解稱做一個流,也就是一條能夠從源點到匯點的一條合法路徑。code
流量:每條邊各自被通過的次數稱做其流量,最終收集的總數爲整個流的流量。blog
容量限制:每條邊的流量不超過其容量(水管會爆的)。
流量平衡:對於除源點和匯點之外的點來講,其流入量必定等於流出量。
如今,咱們先簡化一下這個圖,來解決這個問題。
x/y表示總流量爲y,已經流了x.
首先咱們會想到,隨機找路徑,然而若是走到如上圖所示。
當走完,1->2->3->4咱們就找不到其餘路徑了,那麼答案爲1嗎?不答案爲2.
如今咱們改進算法,給流過的路徑建反向邊,像這樣:
給程序有反悔的機會。
定義一跳變得殘量爲:容量 - 已流過的流量。
反向邊的流量值=正向流過的總流量,也就是說正向流過多少,反向能夠流回多少。
從而咱們又找到1->3->2->4的一條路徑。
再次建路徑上的反向邊,咱們發現沒有路徑能夠到達4點,因此答案爲2.
總結一下上面求最大流的步驟:
1.在圖上找到一條從源點到匯點的路徑(稱爲‘增廣路’)。
2.去增廣路上的殘量最小值v。(也就是流過的路徑中流量最小的那一個)
3.將答案加上v。
4,.將增廣路上全部邊的殘量減去v,反向邊的殘量加上v。
重複上邊4個步驟直到找不到增光路爲止,這稱做 FF 方法。
算法的正確性一會進行證實,咱們先看一下這個算法的效率。
首先這個算法應定不會死循環的,應爲每次增廣都會致使流量增長(而且增長的是整數),並且流量有一個客觀存在最大值,因此它一定結束。(不理解不重要啦QAQ)
因爲咱們並無指定它走哪一條邊,因此優先考慮隨便走一條邊。
咱們考慮一種極限的狀況:
現增廣1->2->3->4,會出現一條3->2容量爲1的邊。
再增廣1->3->2->4,再增廣1->2->3->4....
這浪費大量的時間,若是臉黑的話最多200000次。
然而咱們若是先1->2->4,而後1->3->4,走兩次就行了,上面的作法是咱們不指望的。
咱們能夠考慮每次增廣最短路。
EK算法是以上算法的實現:每次尋找最短路進行增廣。
時間複雜度$O(m2n)$
結構體:儲存三個變量,nxt,to,dis [鄰接表建邊]
flow[ i ] :表示流過 i 點的 v 值,也就是說目前通過到 i 點的路徑上的最小的殘量。
dis[ i ]:表示 i 點距離源點的距離,S,T表示源點以及匯點。
位運算符 ^ :1^1=0 0^1=1 2^1=3 3^1=2.
能夠大體明白它的運算效果。
建邊的時候,爲了方便 ^ 運算符使用,咱們能夠提早建好反向邊,以後一條邊,^ 一下就是另外一條邊了。
首先咱們利用bfs處理圖的連通性以及全部點與源點的距離,固然,當這條邊上的殘量已經爲0的時候,咱們他已經不能通過,咱們能夠直接不考慮。
在bfs中pre數組是記錄每一個點最短路的前驅,last數組記錄上條邊的編號,從而記錄出最短路徑,而後從匯點進行更新便可。
bool bfs(int s,int t) { memset(flow,0x7f,sizeof(flow)); memset(dis,0x7f,sizeof(dis)); memset(vis,0,sizeof(vis)); Q.push(s);vis[s]=1;dis[s]=0,pre[t]=-1; while(!Q.empty()) { int temp=Q.front(); Q.pop(); vis[temp]=0; for(int i=head[temp];i!=-1;i=edge[i].nxt) { int v=edge[i].to; if(edge[i].flow>0&&dis[v]>dis[temp]+edge[i].dis) { dis[v]=dis[temp]+edge[i].dis; pre[v]=temp; last[v]=i; flow[v]=min(flow[temp],edge[i].flow); if(!vis[v]) { vis[v]=1; Q.push(v); } } } } return pre[t]!=-1; }
從匯點向前更新。
while(bfs(s,t)) { int now=t; maxflow+=flow[t]; mincost+=flow[t]*dis[t]; while(now!=s) { edge[last[now]].flow-=flow[t]; edge[last[now]^1].flow+=flow[t]; now=pre[now]; } }
在此以前咱們先了解一個定理:.
什麼是割?
這麼來講吧,有我的住在廢水收集站站附近,他不想然人們江水流到那,晚上偷偷在某個管道處切了一刀,圖成爲不聯通的兩塊,從沒有水流源點流到匯點。
選出一些管道,切斷之後,圖不連通,這些管道的集合就叫割。
這些邊的容量之和叫作這個割的容量。
任取一個割,其容量大於最大流的流量,why?
從源點到匯點每次都會通過割上的最少一條邊。
割掉這條邊之後把源點能到達的邊放在左邊,不能到達的放在右邊。
顯然源點到會點的流量不會超過從左邊走向右邊的次數,而這又不會從左邊到右邊的容量之和。、
直觀一點:
當n管道在一塊兒的時候,你一刀所有切斷,不在一塊兒的時候你也不至於切n+1刀吧。
這個定理如何證實呢?
■考慮FF算法時,殘量網絡上沒有了增廣路。
那麼咱們假設這時候,從源點通過殘量網絡能到達的點組成的集合爲$X$,不能到達的點爲$Y$。顯然匯點在$Y$裏,而且殘量網絡上沒有從$X$到$Y$的邊。
能夠發現如下事實成立:
1.$Y$到$X$的邊的流量爲0.若是不爲0,那麼必定存在一條從X到Y的反向邊,因而矛盾。
2.$X$到$Y$的邊流量等於其容量。只有這樣它纔不會在殘量網絡中出現。
■根據第一個條件得知:沒有流量從$X$到$Y$後又回到$X$。因此當前流量應該等於從$X$到$Y$的邊的流量之和,而根據第二個條件他又等於$X$到$Y$的邊容量之和。
■而全部從X到Y的邊又構成了一個割,其容量等於這些邊的容量之和。
★這意味着咱們找到一個割和一個流,使得前者的流量等於後者的容量。而根據前邊的結論,最大流的流量不超過這個割的容量,因此這個流必定是最大流。
■一樣的,最小割的容量也不會小於這個流的流量,因此這個割也必定是最小割。
■而這也正是FF方法的最後局面,由此咱們對出結論:
(聽說還能夠經過線性規劃對偶定理證實 ...orz)
EK時間複雜度過高,雖然大多數狀況跑不到上界。
有一個顯然的優化:
若是增廣一次後發現最短路沒有變化,那麼能夠繼續增廣,直到源點到匯點的增廣路增大,才須要一邊bfs。
bfs以後咱們去除那些可能在最短路上的邊,即dis[終點]=dis[起點]+1的那些邊。
顯然這些邊構成的圖中沒有環。
咱們只須要延這些邊儘量的增廣便可。
bfs處直接上代碼,比較簡單。
int bfs() { memset(dis,-1,sizeof(dis)); dis[S]=0; Q.push(S); while(!Q.empty()) { int u=Q.front(); Q.pop() ; for(int i=head[u];i!=-1;i=edge[i].nxt) { int v=edge[i].to; if(dis[v]==-1&&edge[i].w>0) { dis[v]=dis[u]+1; //更新 Q.push(v); } } } return dis[T]!=-1; //判斷是否聯通。 }
當圖聯通時進行dfs,目前節點爲u,每次通過與u距離最近的點,而且這條邊的殘量值要大於0,而後日後進行dfs。
咱們在dfs是要加一個變量,做爲流量控制(後邊的流量不能超過前邊流量的最小值)。
dfs中變量flow記錄這條管道以後的最大流量。
bool dfs(int u,int exp) { if(u==T)return exp; //到達重點,所有接受。 int flow=0,tmp=0; for(int i=head[u];i!=-1;i=edge[i].nxt) { int v=edge[i].to; //下一個點。 if(dis[v]==dis[u]+1&&edge[i].w>0) { tmp=dfs(v,min(exp,edge[i].w)); //往下進行 if(!tmp)continue; exp-=tmp; //流量限制-流量,後邊有判斷。 flow+=tmp; edge[i].w-=tmp; //路徑上的邊殘量減小 edge[i^1].w+=tmp; //流經的邊的反向邊殘量增長。 if(!exp)break; //判斷是否在限制邊緣 } } return flow; }
重複上邊若是圖聯通(有最短路徑),就一直進行增廣。
while(bfs())ans+=dfs(S,inf);
Dinic複雜度能夠證實是$O(n2m)$
在某些特殊狀況下(每一個點要麼只有一條入邊且容量爲1,要麼僅有一條出邊且容量爲1)其時間複雜度甚至能作到$O(m \sqrt n )$
#include <iostream> #include <cstring> #include <cstdio> #include <queue> using namespace std; #define inf 0x7fffffff int head[10010],tot; struct ahah{ int nxt,to,w; }edge[100010]; void add(int x,int y,int z) { edge[tot].nxt=head[x]; edge[tot].to=y; edge[tot].w=z; head[x]=tot++; } int n,m,x,y,z; int ans,flow; int dis[10010]; queue <int> Q; int S,T; int bfs() { memset(dis,-1,sizeof(dis)); dis[S]=0; Q.push(S); while(!Q.empty()) { int u=Q.front(); Q.pop() ; for(int i=head[u];i!=-1;i=edge[i].nxt) { int v=edge[i].to; if(dis[v]==-1&&edge[i].w>0) { dis[v]=dis[u]+1; //更新 Q.push(v); } } } return dis[T]!=-1; //判斷是否聯通。 } bool dfs(int u,int exp) { if(u==T)return exp; //到達重點,所有接受。 int flow=0,tmp=0; for(int i=head[u];i!=-1;i=edge[i].nxt) { int v=edge[i].to; //下一個點。 if(dis[v]==dis[u]+1&&edge[i].w>0) { tmp=dfs(v,min(exp,edge[i].w)); //往下進行 if(!tmp)continue; exp-=tmp; //流量限制-流量,後邊有判斷。 flow+=tmp; edge[i].w-=tmp; //路徑上的邊殘量減小 edge[i^1].w+=tmp; //流經的邊的反向邊殘量增長。 if(!exp)break; //判斷是否在限制邊緣 } } return flow; } int main() { memset(head,-1,sizeof(head)); scanf("%d%d%d%d",&n,&m,&S,&T); for(int i=1;i<=m;i++) { scanf("%d%d%d",&x,&y,&z); add(x,y,z);add(y,x,0); //相鄰建邊。 } while(bfs())ans+=dfs(S,inf); printf("%d",ans); }
這優化我也不是太熟悉啦。
當前弧優化的意思就是說每次開始跑鄰接表遍歷不是從第一條邊開始跑而是從上一次點i遍歷跑到的點.
咱們用cur[i]表示這個點,以後每次建完分層圖以後都要進行初始化,且見分層圖時不存在當前弧優化.
int deep[N+1]; int q[N+1]= {0},h,t; int cur[N+1]; bool bfs(int S,int T) { for (int i=0; i<=n; i++) deep[i]=0; //初始化深度爲0 h=t=1; q[1]=S; deep[S]=1; while (h<=t) { for (int i=lin[q[h]]; i; i=e[i].next) if (!deep[e[i].y]&&e[i].v) //若未計算過深度且這條邊不能是空的 { q[++t]=e[i].y; //入隊一個節點 deep[q[t]]=deep[q[h]]+1; //計算深度 } ++h; } if (deep[T]) return true; else return false; } int dfs(int start,int T,int minf) { if (start==T) return minf; //若到了匯點直接返回前面流過來的流量 int sum=0,flow=0; for (int &i=cur[start]; i; i=e[i].next) //當前弧優化,運用指針在修改i的同時,將cur[start]順便修改 if (e[i].v&&deep[start]+1==deep[e[i].y]) { flow=dfs(e[i].y,T,min(minf,e[i].v)); //繼續找增廣路 if (!flow) deep[e[i].y]=0; //去掉已經增廣完的點 sum+=flow; //統計最大流 minf-=flow; //剩餘容量 e[i].v-=flow; e[i^1].v+=flow; //更新剩餘容量 if (!minf) return sum; //若前面已經流完了,直接返回 } return sum; //返回最大流量 } int maxflow(int S,int T) { int sum=0,minf; while (1) //while(1) 控制循環 { if (!bfs(S,T)) return sum; //bfs求出分層圖,順便判斷是否有增廣路 for (int i=1; i<=n; i++) cur[i]=lin[i]; //當前弧的初始化 minf=dfs(S,T,INF); //dfs求出流量 if (minf) sum+=minf; //若流量不爲0,加入 else return sum; //流量爲0,說明沒有增廣路,返回最大流 } }