網絡最大流【附當前弧優化】

一篇巨佬的網絡流建模

網絡最大流問題就是給你一個有向圖,告訴你一個源點與一個匯點,並給每一條邊一個最大流量,需要你求出從源點最多能夠發出多少單位流量到匯點(哎呀我也說不清,就是給你一些或大或小的管道(每個管道都有最大秒流量),一些中轉站,一座供水塔以inf單位每秒的速度供水,問你家每秒最多得到多少單位水(中轉站、供水塔、你家由管道連通))

很顯然看起來我們可以從源點跑dfs,只要到下一節點的邊的邊權>0就跑到下一節點,一路跑過去,直到匯點,然後答案加上這一路上最小的管道流量,這一條路徑上所有管道減去這個流量,然後就沒了。

但是這樣子是有問題的。(藍模板怎麼可能這麼簡單

舉個栗子:(將dfs路徑用紅色表示)

 

 

 

然後你會發現這樣的答案是2,然而正確的答案是4(s-2-t,s-3-t)

不要說你的存邊是2-t比2-3優先,調個位置就卡掉了) 

爲什麼會這樣?

因爲上圖中第一條dfs路徑把原本應是第二條路徑的一條邊給“佔用”了。

計算機可不是人,它無法判斷到底該怎麼跑dfs,所以這時候就需要我們人爲地給計算機一個“反悔”的機會。

重點:怎麼讓計算機“反悔”?

給每條邊建邊權爲0的反向邊,當每次跑到匯點時,在回溯給dfs路徑上的邊權減去最小邊權的時候還要給反向邊加上最小邊權。

先看效果:

 

然後答案就神奇地變成4了! 

爲什麼?

用整體的思想來看:如果我正着經過一條邊,再反着經過,是不是相當於沒經過?好吧,我承認這不是很好理解

這一種“抵消”的思想應用十分廣泛,比如洛谷P1792 [國家集訓隊]種樹,就是一種利用類似“抵消”的方法來實現的可反悔的貪心。

沒理解也沒關係,反正就是要反向建邊,題做多了就理解了QAQ

使用了這種“抵消”思想的dfs,大概就是所謂的EK算法了。(即不停的尋找增廣路然後操作)

然後再介紹一種優化算法:Dinic算法(本質上其實差不多,只不過是一次尋找多條增廣路並更新圖)

PS:增廣路即能夠使答案增加的dfs路徑

大概步驟就是:

1.bfs——給之後尋找增廣路的dfs提供一個bfs序作爲擴展增廣路的依據,同時判斷是否還存在增廣路(如果遍歷不到匯點,就退出輸出答案)。

2.dfs——尋找增廣路(注意這裏一次尋找了多條)並更新答案。

3.重複1

dinic的代碼實現:(貼上我洛谷P3376 【模板】網絡最大流的代碼)

#include<stdio.h>
#include<string.h>
#include<iostream>
#define maxn 20010
#define maxm 200010
#define inf 0x3f3f3f3f
using namespace std;

int bg[maxn],nt[maxm],to[maxm],w[maxm],e=1;
int dep[maxn],q[maxn],n,m,s,t,ans;

void insert(int x,int y,int z) {
    nt[++e]=bg[x];
    to[e]=y;
    w[e]=z;
    bg[x]=e;
}

void bfs() {
    int i,f,l,u,v;
    f=l=1;
    q[1]=s;
    dep[s]=1;
    while (f<=l) {
        u=q[f++];
        for (i=bg[u];i;i=nt[i]) {
            v=to[i];
            if (dep[v] || !w[i]) continue;
            q[++l]=v;
            dep[v]=dep[u]+1;
        }
    }
}

int dfs(int x,int s) {
    int i,u,tmp,res=0;
    if (x==t) return s;
    if (!s) return 0;
    for (i=bg[x];i;i=nt[i]) {
        u=to[i];
        if (dep[u]==dep[x]+1) {
            tmp=dfs(u,min(s,w[i]));
            if (!tmp) continue;
            res+=tmp;
            s-=tmp;
            w[i]-=tmp;
            w[i^1]+=tmp;
            if (!s) break;
        }
    }
    return res;
}

int main() {
    int i,j,x,y,z;
    scanf("%d%d%d%d",&n,&m,&s,&t);
    for (i=1;i<=m;i++) {
        scanf("%d%d%d",&x,&y,&z);
        insert(x,y,z);
        insert(y,x,0);
    }

    while (1) {
        memset(dep,0,sizeof(dep));
        bfs();
        if (!dep[t]) break;
        ans+=dfs(s,inf);
    }
    printf("%d\n",ans);
    return 0;
}

例題鏈接:

洛谷P2764 最小路徑覆蓋問題 我的題解QwQ

洛谷P3355 騎士共存問題 暫時我沒寫題解QAQ

附:當前弧優化

聽名字似乎是一個很高級的東西,但其實很簡單,就幾句話。。。

我們維護一個now數組,在每次dfs前把鏈式前向星的bg數組(或者是head)拷貝一份到now上,然後在dfs枚舉邊找下一個節點時循環枚舉邊的編號不再從bg[x]開始了,而從now[x]開始。而now[x]不斷地更新,即i循環到哪,now[x]都更新爲i。(具體實現還是看代碼吧,我的語言表達能力一向不強

這樣子可以大大提升程序效率。

代碼:

#include<stdio.h>
#include<string.h>
#include<iostream>
#define maxn 20010
#define maxm 200010
#define inf 0x3f3f3f3f
using namespace std;

int bg[maxn],nt[maxm],to[maxm],w[maxm],e=1;
int dep[maxn],q[maxn],n,m,s,t,ans,now[maxn];

void insert(int x,int y,int z) {
    nt[++e]=bg[x];
    to[e]=y;
    w[e]=z;
    bg[x]=e;
}

void bfs() {
    int i,f,l,u,v;
    for (i=1;i<=n;i++) now[i]=bg[i];
    f=l=1;
    q[1]=s;
    dep[s]=1;
    while (f<=l) {
        u=q[f++];
        for (i=bg[u];i;i=nt[i]) {
            v=to[i];
            if (dep[v] || !w[i]) continue;
            q[++l]=v;
            dep[v]=dep[u]+1;
        }
    }
}

int dfs(int x,int s) {
    int i,u,tmp,res=0;
    if (x==t) return s;
    if (!s) return 0;
    for (i=now[x];i;i=nt[i]) {
        u=to[i];
        if (dep[u]==dep[x]+1) {
            tmp=dfs(u,min(s,w[i]));
            if (!tmp) continue;
            res+=tmp;
            s-=tmp;
            w[i]-=tmp;
            w[i^1]+=tmp;
            if (!s) break;
        }
        now[x]=i;
    }
    return res;
}

int main() {
    int i,j,x,y,z;
    scanf("%d%d%d%d",&n,&m,&s,&t);
    for (i=1;i<=m;i++) {
        scanf("%d%d%d",&x,&y,&z);
        insert(x,y,z);
        insert(y,x,0);
    }
    while (1) {
        memset(dep,0,sizeof(dep));
        bfs();
        if (!dep[t]) break;
        ans+=dfs(s,inf);
    }
    printf("%d\n",ans);
    return 0;
}

 實測效率截圖O_O!:

優化之前:

之後:

 

 至於這個優化的正確性,蒟蒻引用一句很6的話(畢竟我描述不出來)——

“對於一次BFS而言,它確定的層次圖中每條邊若已經被走完了,那麼它就不可能再帶來增廣,下一次就直接從這條最後沒走完的邊走就可以了。”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章