1. 程式人生 > >淺談網路最大流

淺談網路最大流

# 網路最大流 #### 目錄 - 前言 - 雙倍經驗 - 網路流初步 - 網路最大流 - $EK$增廣路演算法 - $Dinic$演算法 ------------ #### 前言 這篇題解是當做學習記錄寫的,所以會對網路最大流這個概念進行講解($dalao$們可以忽略蒟蒻$orz$) #### 雙倍經驗$Time$ 1. [洛谷P3376 【模板】](https://www.luogu.com.cn/problem/P3376) ($Ek$演算法 / $Dinic$演算法) 2. [洛谷P2740 [USACO4.2]草地排水Drainage Ditches](https://www.luogu.com.cn/problem/P2740) ------------ #### 網路流初步 這裡主要討論一下網路流演算法可能會涉及到的一些概念性問題 - 定義 對於任意一張**有向圖**(也就是**網路**),其中有$N$個點、$M$條邊以及源點$S$和匯點$T$ 然後我們把$c(x,y)$稱為邊的**容量** - 轉換 為了通俗易懂,我們來結合生活實際理解上面網路的定義: 將有向圖理解為我們城市的**水網**,有$N$戶家庭、$M$條管道以及供水點$S$和匯合點$T$ 是不是好理解一點?現在給出一張網路(圖醜勿怪啊QAQ): ![](https://img2020.cnblogs.com/blog/2055990/202007/2055990-20200709150110449-380364997.png) $S->C->D->E->T$就是該網路的一個流,$2$這個流的流量 - 流函式 和上面的$c$差不多,我們把$f(x,y)$稱為邊的**流量**,則$f$稱為網路的流函式,它滿足三個條件: 1. $s(x,y)≤c(x,y)$ 2. $f(x,y)=-f(y,x)$ 3. $\forall$ $x$≠$S$,$x≠T$, $\sum_{(u,x)∈E }f(u,x)=\sum_{(x,v)∈E }f(x,v)$ 這三個條件其實也是流函式的三大性質: 1. 容量限制:每條邊的流量總不可能大於該邊的容量的(不然水管就爆了) 2. 斜對稱:正向邊的流量=反向邊的流量(反向邊後面會具體講) 3. 流量守恆:正向的所有流量和=反向的所有流量和(就是總量始終不變) - 殘量網路 在任意時刻,網路中所有節點以及剩餘容量大於$0$的邊構成的子圖被稱為**殘量網路** ------------ #### 最大流 對於上面的網路,合法的流函式有很多,其中使得整個網路流量之和最大的流函式稱為網路的**最大流**,此時的流量和被稱為網路的**最大流量** 最大流能解決許多實際問題,比如:一條完整運輸道路(含多條管道)的一次最大運輸流量,還有**二分圖**(蒟蒻還沒學二分圖,學了之後會更新的qwq) 下面就來介紹計算最大流的兩種演算法:$EK$增廣路演算法和$Dinic$演算法 ------------ #### $Edmonds-Karp$增廣路演算法 (為了簡便,習慣稱為$EK$演算法) - 首先來講增廣路是什麼: 若一條從$S$到$T$的路徑上所有邊的剩餘容量都大於0,則稱這樣的路徑為一條**增廣路**(剩餘流量:$c(x,y)-f(x,y)$) - 然後就是$EK$演算法的核心思想啦: 如上,顯然我們可以讓一股流沿著增廣路從$S$流到$T$,然後使網路的流量增大 $EK$演算法的思想就是**不斷用**BFS**尋找增廣路並不斷更新最大流量值,直到網路上不存在增廣路為止** - 再來講理論實現過程: 在$BFS$尋找一條增廣路時,我們只需要考慮**剩餘流量不為$0$的邊**,然後找到一條從$S$到$T$的路徑,同時計算出路徑上**各邊剩餘容量值的最小值$dis$**,則網路的最大流量就可以增加$dis$(**經過的正向邊容量值全部減去$dis$,反向邊全部加上$dis$**) - **反向邊** 插入講解一下反向邊這個概念,這是網路流中的一個重點 為什麼要建反向邊? 因為可能**一條邊可以被包含於多條增廣路徑**,所以為了尋找所有的增廣路經我們就要讓這一條邊有**多次被選擇的機會** 而構建反向邊則是這樣一個機會,相當於給程式一個**反悔**的機會! 為什麼是反悔? 因為我們在找到一個$dis$後,就會對每條邊的容量進行減法操作,而**直接更改值就會影響到之後尋找另外的增廣路**! 還不好理解?那我們舉個~~通俗易懂的~~例子吧: ![](https://img2020.cnblogs.com/blog/2055990/202007/2055990-20200709161217956-287497289.png) 原本$A$到$B$的正邊權是1、反邊權是0,在第一次經過該邊後(假設$dis$值為1),則正邊權變為0,反邊權變為1 當我們需要第二次經過該邊時,我們就能夠通過走反向邊恢復這條邊的原樣(可能有點繞,大家好好理解一下) 以上都是我個人的理解,現在給出《演算法競賽進階指南》上關於反向邊的證明: “**當一條邊的流量$f(x,y)>0$時,根據斜對稱性質,它的反向邊流量$f(y,x)<0$,此時必定有$f(y,x)
using namespace std; int n,m,s,t,u,v; long long w,ans,dis[520010]; int tot=1,vis[520010],pre[520010],head[520010],flag[2510][2510]; struct node { int to,net; long long val; } e[520010]; inline void add(int u,int v,long long w) { e[++tot].to=v; e[tot].val=w; e[tot].net=head[u]; head[u]=tot; e[++tot].to=u; e[tot].val=0; e[tot].net=head[v]; head[v]=tot; } inline int bfs() { //bfs尋找增廣路 for(register int i=1;i<=n;i++) vis[i]=0; queue q; q.push(s); vis[s]=1; dis[s]=2005020600; while(!q.empty()) { int x=q.front(); q.pop(); for(register int i=head[x];i;i=e[i].net) { if(e[i].val==0) continue; //我們只關心剩餘流量>0的邊 int v=e[i].to; if(vis[v]==1) continue; //這一條增廣路沒有訪問過 dis[v]=min(dis[x],e[i].val); pre[v]=i; //記錄前驅,方便修改邊權 q.push(v); vis[v]=1; if(v==t) return 1; //找到了一條增廣路 } } return 0; } inline void update() { //更新所經過邊的正向邊權以及反向邊權 int x=t; while(x!=s) { int v=pre[x]; e[v].val-=dis[t]; e[v^1].val+=dis[t]; x=e[v^1].to; } ans+=dis[t]; //累加每一條增廣路經的最小流量值 } int main() { scanf("%d%d%d%d",&n,&m,&s,&t); for(register int i=1;i<=m;i++) { scanf("%d%d%lld",&u,&v,&w); if(flag[u][v]==0) { //處理重邊的操作(加上這個模板題就可以用Ek演算法過了) add(u,v,w); flag[u][v]=tot; } else { e[flag[u][v]-1].val+=w; } } while(bfs()!=0) { //直到網路中不存在增廣路 update(); } printf("%lld",ans); return 0; } ``` ------------ #### $Dinic$演算法 $EK$演算法每次都可能會遍歷整個殘量網路,但只找出一條增廣路 是不是有點不划算?能不能一次找多條增廣路呢? 答案是可以的:$Dinic$演算法 - 分層圖&$DFS$ 根據$BFS$寬度優先搜尋,我們知道對於一個節點$x$,我們用$d[x]$來表示它的**層次**,即$S$到$x$最少需要經過的邊數。在殘量網路中,滿足$d[y]=d[x]+1$的邊$(x,y)$構成的子圖被稱為**分層圖**(相信大家已經接觸過了吧),而分層圖很明顯是一張有向無環圖 為什麼要建分層圖? 講這個原因之前, 我們還要知道一點:**$Dinic$演算法還需要$DFS$** 現在再放上第一張圖,我們來理解 ![](https://img2020.cnblogs.com/blog/2055990/202007/2055990-20200709150110449-380364997.png) 根據層次的定義,我們可以得出: ``` 第0層:S 第1層:A、C 第2層:B、D 第3層:E、T ``` 在$DFS$中,從$S$開始,每次我們向下一層次隨便找一個點,直到到達$T$,然後再一層一層回溯回去,繼續找這一層的另外的點再往下搜尋 這樣就滿足了我們同時求出多條增廣路的需求! - $Dinic$演算法框架 1. 在殘量網路上$BFS$求出節點的層次,構造分層圖 2. 在分層圖上$DFS$尋找增廣路,在回溯時同時更新邊權 - 適用範圍 時間複雜度:$O(n^2m)$,一般能夠處理$10^4$~$10^5$規模的網路 相較於$EK$演算法,顯然$Dinic$演算法的效率更優也更快:雖然在稀疏圖中區別不明顯,但在稠密圖中$Dinic$的優勢便凸顯出來了(所以$Dinic$演算法用的更多) 此外,$Dinic$演算法求解二分圖最大匹配的時間複雜度為$O(m\sqrt{n})$ - 程式碼$Code$ 這份程式碼是本模板題的AC程式碼,但是使用到了$Dinic$演算法的兩個優化:**當前弧優化+剪枝** ```cpp #include
using namespace std; const long long inf=2005020600; int n,m,s,t,u,v; long long w,ans,dis[520010]; int tot=1,now[520010],head[520010]; struct node { int to,net; long long val; } e[520010]; inline void add(int u,int v,long long w) { e[++tot].to=v; e[tot].val=w; e[tot].net=head[u]; head[u]=tot; e[++tot].to=u; e[tot].val=0; e[tot].net=head[v]; head[v]=tot; } inline int bfs() { //在慘量網路中構造分層圖 for(register int i=1;i<=n;i++) dis[i]=inf; queue q; q.push(s); dis[s]=0; now[s]=head[s]; while(!q.empty()) { int x=q.front(); q.pop(); for(register int i=head[x];i;i=e[i].net) { int v=e[i].to; if(e[i].val>0&&dis[v]==inf) { q.push(v); now[v]=head[v]; dis[v]=dis[x]+1; if(v==t) return 1; } } } return 0; } inline int dfs(int x,long long sum) { //sum是整條增廣路對最大流的貢獻 if(x==t) return sum; long long k,res=0; //k是當前最小的剩餘容量 for(register int i=now[x];i&∑i=e[i].net) { now[x]=i; //當前弧優化 int v=e[i].to; if(e[i].val>0&&(dis[v]==dis[x]+1)) { k=dfs(v,min(sum,e[i].val)); if(k==0) dis[v]=inf; //剪枝,去掉增廣完畢的點 e[i].val-=k; e[i^1].val+=k; res+=k; //res表示經過該點的所有流量和(相當於流出的總量) sum-=k; //sum表示經過該點的剩餘流量 } } return res; } int main() { scanf("%d%d%d%d",&n,&m,&s,&t); for(register int i=1;i<=m;i++) { scanf("%d%d%lld",&u,&v,&w); add(u,v,w); } while(bfs()) { ans+=dfs(s,inf); //流量守恆(流入=流出) } printf("%lld",ans); return 0; } ``` - **當前弧優化** 對於一個節點$x$,當它在$DFS$中走到了第$i$條弧時,前$i-1$條弧到匯點的流一定已經被流滿而沒有可行的路線了 那麼當下一次再訪問$x$節點時,前$i-1$條弧就沒有任何意義了 所以我們可以在每次列舉節點$x$所連的弧時,改變列舉的起點,這樣就可以刪除起點以前的所有弧,來達到優化剪枝的效果 對應到程式碼中,就是$now$陣列 ------------ #### 後序 終於寫完了....現在來特別感謝一些:@那一條變阻器 對於使用$EK$演算法過掉本題的幫助 以及 @取什麼名字 講解$Dinic$演算法的$DFS$部分內容 如果本篇題解有任何錯誤或您有任何不懂的地方,歡迎留言區評論,我會及時回覆、更正,謝謝大家orz! -----