開始總以為網絡流是多么高深的東西,一直不敢去接受,然而學完以后發現好像也不是太難哦,只是好多基礎東西的一些整合。
文章中可能會有多出紕漏,敬請讀者不吝賜教。
我們以一個經典的問題引入算法。
你所在的村庄新開通了地下流水管道,自來水廠源源不斷的提供水,村民們用水直接或間接用水,而村庄用完的廢水統一回收於另一點(設排來的水全部回收)。當然每個管道有一定的容量,廢水站求出最多可以匯聚多少水?
當然這是一個有向圖。
首先明確幾個概念:
容量:每條邊都有一個容量(水管的最大水流容量)
源點:出發點(水廠)。
匯點:結束點(廢水站)。
流:一個合法解稱作一個流,也就是一條可以從源點到匯點的一條合法路徑。
流量:每條邊各自被經過的次數稱作其流量,最終收集的總數為整個流的流量。
圖中會有這么幾個限制:
容量限制:每條邊的流量不超過其容量(水管會爆的)。
流量平衡:對於除源點和匯點以外的點來說,其流入量一定等於流出量。
現在,我們先簡化一下這個圖,來解決這個問題。
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算法:
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]; } }
EK算法還能優化么?
在此之前我們先了解一個定理:.
最大流最小割定理
什么是割?
這么來說吧,有個人住在廢水收集站站附近,他不想然人們江水流到那,晚上偷偷在某個管道處切了一刀,圖成為不聯通的兩塊,從沒有水流源點流到匯點。
選出一些管道,切斷以后,圖不連通,這些管道的集合就叫割。
這些邊的容量之和叫做這個割的容量。
任取一個割,其容量大於最大流的流量,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方法的最后局面,由此我們對出結論:
FF是正確的,並且最小割等於最大流
(據說還可以通過線性規划對偶定理證明 ...orz)
EK優--Dinic
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:
當圖聯通時進行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,說明沒有增廣路,返回最大流 } }