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

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

更壞的閱讀體驗html

定義

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

起點咱們通常稱爲源點,終點通常稱爲匯點算法

內容前置

1.增廣路

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

2.分層

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

3.當前弧優化

​ 每次增廣一條路後能夠看作「榨乾」了這條路,既然榨乾了就沒有再增廣的可能了。但若是每次都掃描這些「枯萎的」邊是很浪費時間的。那咱們就記錄一下「榨取」到那條邊了,而後下一次直接從這條邊開始增廣,就能夠節省大量的時間。這就是當前弧優化函數

具體怎麼實現呢,先把鏈式前向星的head數組複製一份,存進cur數組,而後在cur數組中每次記錄「榨取」到哪條邊了。學習

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

解決算法

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

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

可是這樣的方法有點基於貪心的算法,找到反例是顯而易見的,不必定能夠獲得正解。code

爲了解決這種問題,咱們須要一個能夠吃後悔藥的方法——加反向邊

本來咱們的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

相關文章
相關標籤/搜索