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

網路流-最大流

【前言】

網路流作為一個經典問題,在OI及實際生活中有著廣泛的應用,值得我們仔細研究。

【何為網路流?】

網路流,是一種資源調配問題,如下圖(以下圖片均來自網路):
這裡寫圖片描述
其中,S表示網路流中的源點,是資源的唯一出發點。
T表示網路流中的終點,是各種資源的目的地。
正如水管有粗細之分,道路有寬窄之分,
網路流中的每條邊(這裡稱為弧)都有一個容量cap,表示單位時間最多能通過的資源量。
同時,每條弧都有一個流量flow,表示當前單位時間通過的資源量。
這裡寫圖片描述
上圖是流的一個可行方案。我們的目的就是要求最大流(單位時間到達終點的資源最大)

【後向弧】

我們定義後向弧為與前向弧頂點相同,方向相反的弧。且後向弧的流量永遠為所對應前向弧的流量的相反數。(這裡推薦一個實用方法:把邊從2開始編號,那麼x與x^1就是一對前向/後向弧)

【增廣路】

所謂增廣路,就是能使當前結果更優的一條路。在最大流中,增廣路的定義如下:

1.增廣路由源點出發,終點結束
2.增廣路走過的每一條弧中,cap>flow(即這條路必須所有弧都可以增加流量)
3.增廣路可以經過前向弧,也可以經過後向弧。

這裡寫圖片描述
上圖是一條增廣路。其中BC是一條後向弧。同時也發現原來的狀態不是最大流。
我們可以發現,後向弧為演算法提供了糾正原來錯誤的可能。

【FF方法】

解決最大流的常見方法是FF方法,FF方法可以由很多不同搜尋演算法實現,衍生出很多不同的演算法。
FF方法的過程:

1.初始時,各弧流量為02.尋找一條增廣路,找不到增廣路則說明已經獲得最大流,退出
3.
若找到增廣路,取路上各弧的最小殘餘流量(cap-flow),記為Min 4.把增廣路上各弧流量+Min,其對應弧流量作相應修改 5.重複2~4步驟,直至找不到增廣路

我們可以發現,FF方法其實就是一個不斷糾正,不斷優化的過程。其中尋找增廣路是次方法的時間瓶頸。

【EK演算法】

實際應用時,我們常用BFS來尋找增廣路,稱為EK演算法。

bool bfs(){
    memset(vis,0,sizeof(vis));
    int hed=0,til=1;
    que[1]=s;vis[s]=1;
    while (hed!=til)
     for (int j=a.lnk
[que[++hed]];j;j=a.nxt[j]) if (!vis[a.son[j]]&&a.cap[j]>a.flw[j]){ que[++til]=a.son[j];vis[a.son[j]]=1; fa[a.son[j]]=que[hed];id[a.son[j]]=j; if (a.son[j]==t) return 1; } return 0; } void EK(){ while (bfs()){ int Min=0x3f3f3f3f; for (int x=t;x!=s;x=fa[x]) Min=min(Min,a.cap[id[x]]-a.flw[id[x]]); ans+=Min; for (int x=t;x!=s;x=fa[x]) a.flw[id[x]]+=Min,a.flw[id[x]^1]-=Min; } }

顯然,在最壞情況下,時間複雜度O(NE2)

【Dinic演算法】

實際運用中,我們還有一個更為快捷的演算法求解最大流問題,就是Dinic。

【層次圖】

我們把源點到i所經過的最少邊數稱為i的距離。
距離相同的點歸為同一個層次。
在殘量網路中,只留下連向下一個層次的邊,就構成了一個層次圖。
這裡寫圖片描述

【Dinic流程】

首先,如果有層次圖,就說明當前解還可以增廣
用BFS刷出層次圖,然後就可以直接DFS增廣

BFS很簡單,直接遍歷一下就可以了,不用多說
DFS的過程,可以理解為有一杯無限多的水,不斷從源點倒入
模擬水流的擴張,遍歷整個子圖,並返回當前子圖能進入的最大流量

程式碼是這樣的:

int dfs(int x,int flow){
    if (x==T||flow==0) return flow;
    int res=0,f;
    for (int j=lnk[x];j;j=nxt[j])
     if (d[x]+1==d[son[j]]&&(f=dfs(son[j],min(flow,cap[j]-flw[j])))>0){
        flw[j]+=f;flw[j^1]-=f;
        res+=f;flow-=f;
        if (flow==0) break;
     }
    return res;
}

【當前弧優化】

其實,上面給出的程式碼並不是效率最高的。
試想,對於點x,它的一個兒子s如果已經被DFS增廣過了
那麼下次增廣x的時候,就沒有必要再來增廣s。
因為DFS增廣儘可能地使流量最大,再次DFS就不會再有增廣的可能

所以我們每次建立lnk[]的一個副本pos[]
那麼直接修改pos[]就能達到目的
正確姿勢:

int dfs(int x,int flow){
    if (x==T||flow==0) return flow;
    int res=0,f;
    for (int &j=pos[x];j;j=nxt[j])
     if (d[x]+1==d[son[j]]&&(f=dfs(son[j],min(flow,cap[j]-flw[j])))>0){
        flw[j]+=f;flw[j^1]-=f;
        res+=f;flow-=f;
        if (flow==0) break;
     }
    return res;
}

完整模板如下(hihoCoder 1369):

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=505,maxe=40004,INF=0x3f3f3f3f;
int n,e,S,T;
int tot,lnk[maxn],nxt[maxe],cap[maxe],flw[maxe],son[maxe];
void add(int x,int y,int w){
    son[++tot]=y;cap[tot]=w;flw[tot]=0;nxt[tot]=lnk[x];lnk[x]=tot;
}
inline int red(){
    int tot=0,f=1;char ch=getchar();
    while (ch<'0'||'9'<ch) {if (ch=='-') f=-f;ch=getchar();}
    while ('0'<=ch&&ch<='9') tot=tot*10+ch-48,ch=getchar();
    return tot*f;
}
int d[maxn],pos[maxn],que[maxn];
bool bfs(){
    memset(d,63,sizeof(d));
    int hed=0,til=1;
    d[S]=0;que[1]=S;
    while (hed!=til)
     for (int j=lnk[que[++hed]];j;j=nxt[j])
      if (d[son[j]]==INF&&flw[j]<cap[j])
       que[++til]=son[j],d[son[j]]=d[que[hed]]+1;
    return d[T]!=INF;
}
int dfs(int x,int flow){
    if (x==T||flow==0) return flow;
    int res=0,f;
    for (int &j=pos[x];j;j=nxt[j])
     if (d[x]+1==d[son[j]]&&(f=dfs(son[j],min(flow,cap[j]-flw[j])))>0){
        flw[j]+=f;flw[j^1]-=f;
        res+=f;flow-=f;
        if (flow==0) break;
     }
    return res;
}
int main(){
    n=red(),e=red();S=1;T=n;tot=1;
    for (int i=1,x,y,z;i<=e;i++)
     x=red(),y=red(),z=red(),add(x,y,z),add(y,x,0);
    int ans=0;
    while (bfs()){
        memcpy(pos,lnk,sizeof(lnk));
        ans+=dfs(S,INF);
    }
    printf("%d",ans);
    return 0;
}