前言node
雙倍經驗c++
網絡流初步算法
網絡最大流數組
\(EK\)增廣路算法網絡
\(Dinic\)算法框架
這篇題解是當作學習記錄寫的,因此會對網絡最大流這個概念進行講解(\(dalao\)們能夠忽略蒟蒻\(orz\))函數
洛谷P3376 【模板】 (\(Ek\)算法 / \(Dinic\)算法)學習
這裏主要討論一下網絡流算法可能會涉及到的一些概念性問題spa
對於任意一張有向圖(也就是網絡),其中有\(N\)個點、\(M\)條邊以及源點\(S\)和匯點\(T\)
而後咱們把\(c(x,y)\)稱爲邊的容量
爲了通俗易懂,咱們來結合生活實際理解上面網絡的定義:
將有向圖理解爲咱們城市的水網,有\(N\)戶家庭、\(M\)條管道以及供水點\(S\)和匯合點\(T\)
是否是好理解一點?如今給出一張網絡(圖醜勿怪啊QAQ):
\(S->C->D->E->T\)就是該網絡的一個流,\(2\)這個流的流量
和上面的\(c\)差很少,咱們把\(f(x,y)\)稱爲邊的流量,則\(f\)稱爲網絡的流函數,它知足三個條件:
\(s(x,y)≤c(x,y)\)
\(f(x,y)=-f(y,x)\)
\(\forall\) \(x\)≠\(S\),\(x≠T\), \(\sum_{(u,x)∈E }f(u,x)=\sum_{(x,v)∈E }f(x,v)\)
這三個條件其實也是流函數的三大性質:
容量限制:每條邊的流量總不可能大於該邊的容量的(否則水管就爆了)
斜對稱:正向邊的流量=反向邊的流量(反向邊後面會具體講)
流量守恆:正向的全部流量和=反向的全部流量和(就是總量始終不變)
在任意時刻,網絡中全部節點以及剩餘容量大於\(0\)的邊構成的子圖被稱爲殘量網絡
對於上面的網絡,合法的流函數有不少,其中使得整個網絡流量之和最大的流函數稱爲網絡的最大流,此時的流量和被稱爲網絡的最大流量
最大流能解決許多實際問題,好比:一條完整運輸道路(含多條管道)的一次最大運輸流量,還有二分圖(蒟蒻還沒學二分圖,學了以後會更新的qwq)
下面就來介紹計算最大流的兩種算法:\(EK\)增廣路算法和\(Dinic\)算法
(爲了簡便,習慣稱爲\(EK\)算法)
若一條從\(S\)到\(T\)的路徑上全部邊的剩餘容量都大於0,則稱這樣的路徑爲一條增廣路(剩餘流量:\(c(x,y)-f(x,y)\))
如上,顯然咱們可讓一股流沿着增廣路從\(S\)流到\(T\),而後使網絡的流量增大
\(EK\)算法的思想就是不斷用BFS尋找增廣路並不斷更新最大流量值,直到網絡上不存在增廣路爲止
在\(BFS\)尋找一條增廣路時,咱們只須要考慮剩餘流量不爲\(0\)的邊,而後找到一條從\(S\)到\(T\)的路徑,同時計算出路徑上各邊剩餘容量值的最小值\(dis\),則網絡的最大流量就能夠增長\(dis\)(通過的正向邊容量值所有減去\(dis\),反向邊所有加上\(dis\))
插入講解一下反向邊這個概念,這是網絡流中的一個重點
爲何要建反向邊?
由於可能一條邊能夠被包含於多條增廣路徑,因此爲了尋找全部的增廣路經咱們就要讓這一條邊有屢次被選擇的機會
而構建反向邊則是這樣一個機會,至關於給程序一個反悔的機會!
爲何是反悔?
由於咱們在找到一個\(dis\)後,就會對每條邊的容量進行減法操做,而直接更改值就會影響到以後尋找另外的增廣路!
還很差理解?那咱們舉個通俗易懂的例子吧:
本來\(A\)到\(B\)的正邊權是一、反邊權是0,在第一次通過該邊後(假設\(dis\)值爲1),則正邊權變爲0,反邊權變爲1
當咱們須要第二次通過該邊時,咱們就可以經過走反向邊恢復這條邊的原樣(可能有點繞,你們好好理解一下)
以上都是我我的的理解,如今給出《算法競賽進階指南》上關於反向邊的證實:
「當一條邊的流量\(f(x,y)>0\)時,根據斜對稱性質,它的反向邊流量\(f(y,x)<0\),此時一定有\(f(y,x)<c(y,x)\),因此\(EK\)算法除了遍歷原圖的正向邊之外還要考慮遍歷每條反向邊」
咱們將正向邊和反向邊存在「2和3」、「4和5」、「6和7」····
爲何?
由於在更新邊權的時候,咱們就能夠直接使用\(xor 1\)的方式,找到對應的正向邊和反向邊(奇數異或1至關於-1,偶數異或1至關於+1)
代碼實現以下(整個更新邊權的操做函數):
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]; }
時間複雜度爲\(O(nm^2)\),通常能處理\(10^3\)~\(10^4\)規模的網絡
(以本道模板題的代碼爲準,其餘題能夠將\(longlong\)換成\(int\)而且能夠去掉處理重邊操做)
#include <bits/stdc++.h> 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<int> 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; }
\(EK\)算法每次均可能會遍歷整個殘量網絡,但只找出一條增廣路
是否是有點不划算?能不能一次找多條增廣路呢?
答案是能夠的:\(Dinic\)算法
根據\(BFS\)寬度優先搜索,咱們知道對於一個節點\(x\),咱們用\(d[x]\)來表示它的層次,即\(S\)到\(x\)最少須要通過的邊數。在殘量網絡中,知足\(d[y]=d[x]+1\)的邊\((x,y)\)構成的子圖被稱爲分層圖(相信你們已經接觸過了吧),而分層圖很明顯是一張有向無環圖
爲何要建分層圖?
講這個緣由以前, 咱們還要知道一點:\(Dinic\)算法還須要\(DFS\)
如今再放上第一張圖,咱們來理解
根據層次的定義,咱們能夠得出:
第0層:S 第1層:A、C 第2層:B、D 第3層:E、T
在\(DFS\)中,從\(S\)開始,每次咱們向下一層次隨便找一個點,直到到達\(T\),而後再一層一層回溯回去,繼續找這一層的另外的點再往下搜索
這樣就知足了咱們同時求出多條增廣路的需求!
在殘量網絡上\(BFS\)求出節點的層次,構造分層圖
在分層圖上\(DFS\)尋找增廣路,在回溯時同時更新邊權
時間複雜度:\(O(n^2m)\),通常可以處理\(10^4\)~\(10^5\)規模的網絡
相較於\(EK\)算法,顯然\(Dinic\)算法的效率更優也更快:雖然在稀疏圖中區別不明顯,但在稠密圖中\(Dinic\)的優點便凸顯出來了(因此\(Dinic\)算法用的更多)
此外,\(Dinic\)算法求解二分圖最大匹配的時間複雜度爲\(O(m\sqrt{n})\)
這份代碼是本模板題的AC代碼,可是使用到了\(Dinic\)算法的兩個優化:當前弧優化+剪枝
#include <bits/stdc++.h> 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<int> 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!