1. 程式人生 > 其它 >[學習筆記] 網路流最大流(Dinic演算法)

[學習筆記] 網路流最大流(Dinic演算法)

網路流最大流

Dinic 演算法

先回憶一下\(EK\)演算法,\(bfs\)遍歷整張圖直到找到一條增廣路,每次只能找一條,速度就比較慢,所以我們就得使用

更好的\(dinic\)演算法,\(EK\)一次找一條,但是源點到匯點可能有很多條最短路經,所以我們可以一次性全部擴充套件

首先我們需要知道分層圖:滿足\(d[y]=d[x]+1d[y]=d[x]+1\)的邊\((x,y)\)構成的子圖被稱為分層圖

\(d[x]\)表示的就是點\(x\)的層次,也就是\(S\)\(x\)最到需要經過的邊數(和流量沒有關係)

至於為什麼要分層,問就是防止\(dfs\)亂走

演算法流程

先進行一邊\(bfs\)

求出源點到每個點的最短邊樹\(d_x\),然後將圖分層,每次制考慮 \(d_y=d_x+1\) 的邊\((x,y)\)

每次\(dfs\),從\(S\)開始攜帶\(∞\)的流往下遞迴,遞迴時列舉當前節點的所有相鄰的點,並儘可能將有的流發給相鄰的點

並繼續遞迴,遞迴結束後更新現有的流,如果沒有流或者所有相鄰的點都訪問完了就直接返回,如果到了匯點\(T\)

就把所有的現有的流都送出去

簡單來說就是

  1. \(bfs\)求出每個點的\(d_x\)給每個點一個層次,得到一張分層圖
  2. 根據層次反覆\(dfs\),每次嘗試給予時,只考慮給予自己下一層的點,就可以防止混亂

當前弧優化

從前面的介紹可以知道,這個\(dinic\)

分明就是\(dfs\),既然是\(dfs\)沒有剪枝,那它就是指數級暴力(

對於一個節點\(x\),由於有效邊構成的字圖一定是\(DAG\)(有向無環圖),所以走到第\(i\)條弧時,前\(i-1\)條弧到匯點

的流都一定流滿了,沒有可行的路線了,也就是說每處理完一條從\(x\)出發的弧,以後就不用在訪問了

於是我們考慮開一個數組\(now_x\)表示當前考慮到了哪一條弧(不是某個點),使用方法和\(head\)是相同的,這樣就

可以改變每次搜尋的起點了,由於大前提是在分層圖中,所以每次\(bfs\) 就把\(now_x\)設為鄰接表的表頭

\(Q:\)為什麼當前弧優化不寫成\(now[u]=nxt[i]\)

,難道不會重複遍歷一條邊嗎?

\(A:\)除非當前這條邊到源點了或者該流的都流完了,否則都有多流一些的可能,而且從名字“當前”也能看出吧(

code

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <map>
#include <queue>
#define int long long 
using namespace std;
const int inf=0x3f3f3f3f;
const int N=1e4+10;
int n,m,s,t,head[N],nxt[N],val[N],tot=1,d[N],now[N],ver[N];
//注意tot的初始值要設為1,因為0^1=0
inline int read(){
    int x=0,f=1;char ch=getchar();
    while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
    return x*f;
}
inline void add(int x,int y,int z){
    ver[++tot]=y,nxt[tot]=head[x],val[tot]=z,head[x]=tot;
}
inline bool bfs(){
    for(int i=1;i<=n;i++) d[i]=inf,now[i]=head[i];
    queue<int> q;
    q.push(s);
    d[s]=0;
    while(!q.empty()){
	int u=q.front();q.pop();
	for(int i=head[u];i;i=nxt[i]){
	    int v=ver[i];
	    if(val[i]&&d[v]==inf){//滿足增廣路的性質且沒有被訪問過
		q.push(v);
		d[v]=d[u]+1;//分層
		if(v==t) return 1;//找到增廣路
	    }
	}
    }
    return 0;
}
inline int dinic(int u,int sum){//求單次增廣送出去的流量,sum
    if(u==t) return sum;//到匯點,直接把全部送出去
    int ret=0;//目前送出去多少流
    for(int i=now[u];i;i=nxt[i]){
	now[u]=i;//當前弧優化
	int v=ver[i];
	if(val[i]&&d[v]==d[u]+1){//只考慮下一層的點且滿足增廣路的性質
	    int k=dinic(v,min(sum-ret,val[i]));//k是v送出去的流
	    val[i]-=k;val[i^1]+=k;//遞迴更新現有的流
	    ret+=k;
	    if(ret==sum) return sum;//如果全部送完就直接返回
	}
    }
    return ret;
}
signed main(){
    n=read();m=read();s=read();t=read();
    for(int i=1;i<=m;i++){
	int u=read(),v=read(),w=read();
	add(u,v,w);
	add(v,u,0);
    }
    int res=0;
    while(bfs()) res+=dinic(s,inf);
    cout<<res<<endl;
    return 0;
}

參考資料

詳細的部落格——ET2006

碼風近似且易懂——摸魚醬

對反邊的解釋比較好——Eleven謙

多——辰星凌

日報