三種網絡流(最大流)的實現算法講解與代碼


[洛谷P3376題解]網絡流(最大流)的實現算法講解與代碼

更壞的閱讀體驗

定義

對於給定的一個網絡,有向圖中每個的邊權表示可以通過的最大流量。假設出發點S水流無限大,求水流到終點T后的最大流量。

起點我們一般稱為源點,終點一般稱為匯點

內容前置

1.增廣路

​ 在一個網絡源點S匯點T的一條各邊剩余流量都大於0(還能讓水流通過,沒有堵住)的一條路。

2.分層

​ 預處理出源點到每個點的距離(每次尋找增廣路都要,因為以前原本能走的路可能因為水灌滿了,導致不能走了).作用是保證只往更遠的地方放水,避免兜圈子或者是沒事就走回頭路(正所謂人往高處走水往低處流).

3.當前弧優化

​ 每次增廣一條路后可以看做“榨干”了這條路,既然榨干了就沒有再增廣的可能了。但如果每次都掃描這些“枯萎的”邊是很浪費時間的。那我們就記錄一下“榨取”到那條邊了,然后下一次直接從這條邊開始增廣,就可以節省大量的時間。這就是當前弧優化

具體怎么實現呢,先把鏈式前向星的head數組復制一份,存進cur數組,然后在cur數組中每次記錄“榨取”到哪條邊了。

[#3 引用自](Dinic當前弧優化 模板及教程 - Floatiy - 博客園 (cnblogs.com))

解決算法

Ford-Fulkerson 算法(以下簡稱FF算法)

FF算法的核心是找增廣路,直到找不到為止。(就是一個搜索,用盡可能多的水流填充每一個點,直到沒有水用來填充,或者沒有多余的節點讓水流出去)。

但是這樣的方法有點基於貪心的算法,找到反例是顯而易見的,不一定可以得到正解。

為了解決這種問題,我們需要一個可以吃后悔葯的方法——加反向邊

原本我們的DFS是一條路走到黑的,現在我們每次進入一個節點,把水流送進去,同時建立一個權值與我們送入的水流量相等,但是方向相反的路(挖一條路讓水流能夠反向流回來,相當於給水流吃一顆后悔葯)。

我們給了FF算法一顆后悔葯之后就可以讓他能夠找到正確的最大流。

Ford-Fulkerson算法的復雜度為\(O(e \times f)\) ,其中 \(e\) 為邊數, \(f\)為最大流

上代碼。

#include <iostream>
#include <cstring>
using namespace std;

#define INF 0x3f3f3f3f3f3f3f3f

typedef long long ll;

// Base
const int N= 256;
const int M= 8192*2;
// End

// Graph
int head[N],nxt[M],to[M];
ll dis[M];
int p;

inline void add_edge(int f,int t,ll d)
{
    to[p]=t;
    dis[p]=d;
    nxt[p]=head[f];
    head[f]=p++;
}
// End

int n,m,s,t;

// Ford-Fulkerson

bool vis[N];



ll dfs(int u,ll flow)//u是當前節點 , flow是送過來的水量
{
    if(u==t)// End,水送到終點了
        return flow;
    vis[u]=true;

    for(int i=head[u];i!=-1;i=nxt[i])
    {
        ll c;//存 送水下一個節點能通到終點的最大流量
        if(dis[i]>0 //如果水流還能繼續流下去
            && !vis[to[i]]  //並且要去的點沒有其他的水流去過
            && (c=dfs(to[i],min(flow,dis[i])))!=-1//根據木桶效應,能傳給下一個節點的水量取決於當前節點有的水量與管道(路徑)能夠輸送的水量的最小值
            //要保證這條路是通的我們才可以向他送水,不然就是浪費了
            ) {
                dis[i]-=c;//這個管道已經被占用一部分用來送水了,需要減掉
                dis[i^1]+=c;//給他的反向邊加上相同的水量,送后悔葯
                //至於為什么是這樣取出反向邊,下面有講
                return c;
        }
    }
    return -1;
}
// End
int main()
{
    ios::sync_with_stdio(true);
    
    memset(head,-1,sizeof(head));// init

    cin>>n>>m>>s>>t;
    for(int i=1;i<=m;i++)
    {
        int u,v,w;cin>>u>>v>>w;
        add_edge(u,v,w);
        add_edge(v,u,0);//建立一條暫時無法通水的反向邊(后面正向邊送水后,需要加上相同的水量)
        //第一條邊 編號是 0 ,其反向邊為 1, 眾所周知的 奇數^1=奇數-1, 偶數^1=偶數+1 ,利用這種性質 ,我們就可以很快求出反向邊,或者反向邊得出正向邊(這里說的正反只是相對)
    }

    //Ford-Fulkerson
    ll ans = 0;
    ll c;
    //          假設我們的水無限多
    while((c=dfs(s,INF)) != -1) //把源點還能送出去的水全部都送出去,直到送不到終點
    {
        memset(vis,0,sizeof(vis)); //重新開始送沒送出去的水
        ans+=c;//記錄總的水量
    }
    cout<<ans<<endl;
    return 0;
}

可以看出效率比較低,我這里開了O2也過不了模板題。

Edmond-Karp 算法(以下簡稱EK算法)

上面FF算法太慢了,原因是因為FF算法太死腦筋了,非要等現在節點水灌滿了,才會灌其他的(明明有一個更大的水管不灌)。另外他有時候還非常謙讓,等到明明走到了,卻要返回去等別人水灌好,再灌自己的。

其實,EK算法便是FF算法的BFS版本。復雜度為\(O(v \times e^2)\)​(復雜度這么高行得通嗎,當然可以,事實上一般情況下根本達不到這么高的上限)。

那我就直接上代碼了。

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;

#define INF 0x3f3f3f3f3f3f3f3f

typedef long long ll;

// Base
const int N= 256;
const int M= 8192*2;
// End

// Graph
int head[N],nxt[M],to[M];
ll dis[M];
int p;

inline void add_edge(int f,int t,ll d)
{
    to[p]=t;
    dis[p]=d;
    nxt[p]=head[f];
    head[f]=p++;
}
// End

int n,m,s,t;

// Edmond-Karp
int last[N];
ll flow[N];//記錄當前的點是哪條邊通到來的,這樣多余的水又可以這樣送回去.

inline bool bfs() //水還能送到終點返回true,反之false
{
    memset(last,-1,sizeof last);
    queue <int > Q;
    Q.push(s);
    flow[s] = INF; //把起點的水量裝填到無限大
    while(!Q.empty())
    {
        int k=Q.front();
        Q.pop();
        if(k==t)// End,水送到終點了
            break;
        for(int i=head[k];i!=-1;i=nxt[i])
        {
            if(dis[i]>0 //如果水流還能繼續流下去
               && last[to[i]]==-1  //並且要去的點沒有其他的水流去過,所以last[to[i]]還是-1
               ){
                last[to[i]]=i;  // 到 to[i]點 需要走 i這條邊
                flow[to[i]]=min(flow[k],dis[i]);//根據木桶效應,能傳給下一個節點的水量取決於當前節點有的水量與管道(路徑)能夠輸送的水量的最小值
                Q.push(to[i]);  //入隊
            }
        }
    }
    return last[t]!=-1;//能夠送到終點
}
// End


int main()
{
    ios::sync_with_stdio(true);
    memset(head,-1,sizeof(head));// init

    cin>>n>>m>>s>>t;
    for(int i=1;i<=m;i++)
    {
        int u,v,w;cin>>u>>v>>w;
        add_edge(u,v,w);
        add_edge(v,u,0);//建立一條暫時無法通水的反向邊(后面正向邊送水后,需要加上相同的水量)
        //第一條邊 編號是 0 ,其反向邊為 1, 眾所周知的 奇數^1=奇數-1, 偶數^1=偶數+1 ,利用這種性質 ,我們就可以很快求出反向邊,或者反向邊得出正向邊(這里說的正反只是相對)
    }
    // Edmond-Karp
    ll maxflow=0;
    while(bfs())//把源點還能送出去的水全部都送出去,直到送不到終點
    {
        maxflow+=flow[t];
        for(int i=t;i!=s;i=to[last[i]^1])//還有多余的水殘留在管道里,怪可惜的,原路送回去.
        {
            dis[last[i]]-=flow[t];  //這個管道已經被占用一部分用來送水了,需要減掉
            dis[last[i]^1]+=flow[t];    //給他的反向邊加上相同的水量,送后悔葯
            //至於為什么是這樣取出反向邊,上面有講
        }
    }
    cout<<maxflow<<endl;
    //
    return 0;
}

於是我們AC了這題。

還能不能更快? Dinic算法

FFEK算法都有個比較嚴重的問題.他們每次都只能找到一條增廣路(到終點沒有被堵上的路).Dinic算法不僅用到了DFS,還用的了BFS.但是他們發揮的作用是不一樣的。

種類 作用
DFS 尋找路
BFS 分層(內容前置里有講哦)

Dinic快就快在可以多路增廣(兵分三路把你干掉),這樣我們可以節省很多走重復路徑的時間.當找到一條增廣路后,DFS會嘗試用剩余的流量向其他地方擴展.找到新的增廣路。

就這???

當然不止,Dinic還有當前弧優化(前面也有哦),總之就是放棄被榨干的路。

這樣的一通操作之后,復雜度來到了\(O(v^2 \times e)\)

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;

#define INF 0x3f3f3f3f3f3f3f3f

typedef long long ll;

// Base
const int N = 256;
const int M = 8192 * 2;
// End

// Graph
int head[N], nxt[M], to[M];
ll dis[M];
int p;

inline void add_edge(int f, int t, ll d)
{
    to[p] = t;
    dis[p] = d;
    nxt[p] = head[f];
    head[f] = p++;
}
// End

int n, m, s, t;

//Dinic
int level[N], cur[N];
//level是各點到起點的深度,cur為當前弧優化的增廣起點

inline bool bfs() //分層函數,其實就是個普通廣度優先搜索,沒什么好說的,作用是計算邊權為1的圖,圖上各點到源點的距離
{
    memset(level, -1, sizeof(level));
    level[s] = 0;
    memcpy(cur, head, sizeof(head));
    cur[s]=head[s];
    queue<int> Q;
    Q.push(s);
    while (!Q.empty())
    {
        int k = Q.front();
        Q.pop();

        for (int i = head[k]; i != -1; i = nxt[i])
        {
            //還能夠通水的管道才有價值
            if (dis[i] > 0 && level[to[i]] == -1)
            {
                level[to[i]] = level[k] + 1;
                Q.push(to[i]);
                if(to[i]==t) return true;
            }
        }
    }
    return false;
}

ll dfs(int u, ll flow)
{
    if (u == t)
        return flow;

    ll flow_now = flow; // 剩余的流量
    for (int i = cur[u]; i != -1 && flow_now > 0; i = nxt[i])
    {
        cur[u] = i; //當前弧優化

        //如果水流還能繼續流下去   並且    是向更深處走的
        if (dis[i] > 0 && level[to[i]] == level[u] + 1)
        {
            ll c = dfs(to[i], min(dis[i], flow_now));
            if(!c) level[to[i]]=-1;  //剪枝,去除增廣完畢的點
        
            flow_now -= c;  //剩余的水流被用了c

            dis[i] -= c;    //這個管道已經被占用一部分用來送水了,需要減掉
            dis[i ^ 1] += c;    //給他的反向邊加上相同的水量,送后悔葯
            //至於為什么是這樣取出反向邊,下面有講
        }
    }
    return flow - flow_now; //返回用掉的水流
}

//End

int main()
{
    ios::sync_with_stdio(true);
    memset(head, -1, sizeof(head)); // init

    cin >> n >> m >> s >> t;
    for (int i = 1; i <= m; i++)
    {
        int u, v, w;
        cin >> u >> v >> w;
        add_edge(u, v, w);
        add_edge(v, u, 0); //建立一條暫時無法通水的反向邊(后面正向邊送水后,需要加上相同的水量)
        //第一條邊 編號是 0 ,其反向邊為 1, 眾所周知的 奇數^1=奇數-1, 偶數^1=偶數+1 ,利用這種性質 ,我們就可以很快求出反向邊,或者反向邊得出正向邊(這里說的正反只是相對)
    }

    //Dinic
    ll ans = 0;
    while (bfs())
        ans += dfs(s, INF);
    cout << ans << endl;
    return 0;
}

這個算法如果應用在二分圖里,復雜度為\(O(v \times sqrt(e))\)

參考文獻

1.《算法競賽進階指南》作者:李煜東

2.《[算法學習筆記(28): 網絡流](算法學習筆記(28): 網絡流 - 知乎 (zhihu.com))》 作者:Pecco

3.《[Dinic當前弧優化 模板及教程](Dinic當前弧優化 模板及教程 - Floatiy - 博客園 (cnblogs.com))》作者:Floatiy


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM