最大流算法之Dinic

引言
在最大流(一)中咱們討論了關於EK算法的原理與代碼實現,此文將討論與EK算法同級別複雜度(O(N^2M))的算法——Dinic算法。
Dinic算法用到的思想是圖的分層結構,經過BFS將每個節點標出層次後DFS獲得當前增廣路。而後繼續在殘留網絡中進行BFS分層,當匯點不在層次網絡時(沒有連通弧了),算法結束。c++


Dinic算法結構
0.初始化(邊表)
1.BFS分層——匯點不在層次網絡中跳出
2.DFS尋找增廣路
3.輸出最大流算法


關於初始化
在最大流(一)中,在MLE的狀況下咱們捨棄了鄰接矩陣的存儲方式轉而使用邊表存儲每一條弧的信息。存儲方式與(一)中相同。
即定義結構體:markdown

struct qi
{
    int st, en, num;//首、尾、流量限制
}flow[maxm];//邊表 

採用讀入優化讀取數據。網絡

int read()//讀入優化 
{
    char a;
    int input = 0;
    a = getchar();
    while(a < '0' || a > '9')
        a = getchar();
    while(a >= '0' && a <= '9')
    {
        input = input*10+a-'0';
        a = getchar();
    }
    return input;
}

將每一條弧與序號一一對應函數

for(i = 0; i != m; ++i)
{
    low[i].st = read(), flow[i].en = read(), flow[i].num = read();
    re[flow[i].st][++num[flow[i].st]] = i;//編號與弧的一一映射 
    re[flow[i].en][++num[flow[i].en]] = m+i;//定義反向弧的編號與該弧的關係 
}

初始化反向弧信息:優化

for(i = m; i != m+m; ++i)//反向弧流量限制初始爲0 
{
    flow[i].st = flow[i-m].en;
    flow[i].en = flow[i-m].st;
    flow[i].num = 0;
}

如此初始化完成。ui


BFS分層:
概念:所謂分層就是將圖的每個節點按照某一個標準分類。在Dinic算法中,標準是每個節點到源點的最短路徑(通過幾條弧),由此獲得每個節點的層級(源點層次爲0,以此類推)。
目的:限制每一次尋找增廣路的時候使其在尋找增廣路時不會出現浪費。若i -> j,需知足lev[j] = lev[i]+1。
實現方法廣度優先搜索。記錄每個與之相鄰的節點的等級爲cur+1,每一個節點每次廣搜中只遍歷一次spa

由此可獲得兩種結果
1.匯點的層次是N(N > 0);
2.匯點沒有層次(N == 0);
結果爲2時咱們結束算法,由於在殘餘網絡中已不存在增廣路了。
若是結果是1咱們繼續進行操做——DFS尋找增廣路(以下)。code


關於DFS尋找增廣路:regexp

注意深搜在if中。

int Dfs(int curr, int min_flow)//尋找增廣路
{
    int i, j, a = 0;
    if(curr == en)  return min_flow;//遍歷到匯點返回
    for(i = start[curr]; i != num[curr]+1; ++i)//每個與之相連的節點
    {
        ++start[curr];//當前弧優化
        j = flow[re[curr][i]].en;//當前節點的下一個節點
        if(lev[j] == lev[curr]+1 && (a = Dfs(j, min(min_flow, flow[re[curr][i]].num))))//here
        {
            flow[re[curr][i]].num -= a;
            flow[re[curr][i]+m].num += a;
            if(min_flow == 0)   break;//優化,當找不到增廣路時直接跳出
            return a;
        }
    }   
    return 0;//遍歷不到退出
}

解釋一下if的條件。

if(lev[j] == lev[curr]+1 && (a = Dfs(j, min(min_flow, flow[re[curr][i]].num))))

首先根據算法應該知足級數比當前級數大1;
其次進行深搜,實際上循環中的代碼只是用來更新流量的,經過回溯更新當前增廣路的流量限制。真正尋找增廣路的部分在於後面括號中的代碼

也就是

(a = Dfs(j, min(min_flow, flow[re[curr][i]].num))

當a != 0 時,會返回true(!0),獲得0時會返回false,當返回false的時候說明當前路徑不是增廣路,故直接跳出。(函數最後的return 0)


關於當前弧優化:
由於每個節點可能有多個節點與之相連,故DFS遍歷的時候可能會再次訪問到。
例子:
節點P**已經被遍歷,並且已經遍歷到與之相連的第二個節點。被再次遍歷到,說明要繼續遍歷與之相鄰的節點,由於前兩個節點(在本個例子中)已經在以前已經遍歷過,因此應該直接從第三個開始遍歷。故用一個start[i]記錄已經遍歷到的相鄰節點個數(也是第幾個),使得在未來訪問時不重複深搜已遍歷節點。


Dinic:

int Dinic(int st_pos, int end_pos)
{
    int i, minn, max_flow = 0;
    while(Bfs(st, en))
    {
        memset(start, 0, sizeof start);//每次深搜將上次已遍歷節點數清零
        while(minn = Dfs(st, INF))  max_flow += minn;
    }
    return max_flow;
}

這個就是以前整個算法的結構的代碼形式


完整代碼:

/*
Algorithm: Dinic
Author: kongse_qi
date: 2017/04/09
*/

#include <bits/stdc++.h>
#define INF 0x3f3f3f
#define maxm 200005
#define maxn 10005
using namespace std;

int n, m, st, en, num[maxn], re[maxn][maxn/10], lev[maxn], minn, start[maxn];
struct qi{int st, en, num;}flow[maxm];//邊表 
bool wh[maxn];

int read()//讀入優化 
{
    char a;
    int input = 0;
    a = getchar();
    while(a < '0' || a > '9')
        a = getchar();
    while(a >= '0' && a <= '9')
    {
        input = input*10+a-'0';
        a = getchar();
    }
    return input;
}

void Init()//初始化 
{
    int i;
    memset(num, -1, sizeof num);
    n = read(), m = read(), st = read(), en = read();
    for(i = 0; i != m; ++i)
    {
        flow[i].st = read(), flow[i].en = read(), flow[i].num = read();
        re[flow[i].st][++num[flow[i].st]] = i;//編號與弧的一一映射 
        re[flow[i].en][++num[flow[i].en]] = m+i;//定義反向弧的編號與該弧的關係 
    }
    for(i = m; i != m+m; ++i)//反向弧流量限制初始爲0 
    {
        flow[i].st = flow[i-m].en;
        flow[i].en = flow[i-m].st;
        flow[i].num = 0;
    }
    return ;
}

bool Bfs(int st, int en)//BFS將圖分層 
{
    int i, j, ne, st_pos = -1, end_pos = 0, curr_pos, q[maxn], tot = 1;
    bool wh_con = 0;
    lev[st] = 0;//初始源點層數爲0 
    memset(wh, 0, sizeof wh);
    memset(lev, 0, sizeof lev);
    wh[st] = 1;
    q[0] = st; 
    while(st_pos != end_pos)
    {
        curr_pos = q[++st_pos];
        for(i = 0; i != num[curr_pos]+1; ++i)
        {
            j = re[curr_pos][i];//當前弧 
            ne = flow[j].en;//當前弧的終點 
            if(!wh[flow[j].en] && flow[j].num > 0)//流量限制>0 && 這次未遍歷 
            {
                if(ne == en)    wh_con = 1;//源點在殘餘網絡之中 
                wh[ne] = 1;
                q[++end_pos] = ne;
                lev[ne] = lev[curr_pos]+1; 
                ++tot;
            }
            if(tot == n)    return 1;//優化1:整個網絡完成遍歷後直接退出 
        }
    }
    return wh_con;
}

int Dfs(int curr, int min_flow)//尋找堵塞流 
{
    int i, j, a = 0;
    if(curr == en || min_flow == 0) return min_flow;
    for(i = start[curr]; i != num[curr]+1; ++i)
    {
        ++start[curr];
        j = flow[re[curr][i]].en;
        if(lev[j] == lev[curr]+1 && (a = Dfs(j, min(min_flow, flow[re[curr][i]].num))))
        {
            flow[re[curr][i]].num -= a;
            flow[re[curr][i]+m].num += a;
            if(min_flow == 0)   break;
            return a;
        }
    }   
    return 0;   
}

int Dinic(int st_pos, int end_pos)
{
    int i, minn, max_flow = 0;
    while(Bfs(st, en))
    {
        memset(start, 0, sizeof start);
        while(minn = Dfs(st, INF))  max_flow += minn;
    }
    return max_flow;
}

int main()
{
    //freopen("test.in", "r", stdin);
    //freopen("test.out", "w", stdout);

    Init();
    printf("%d", Dinic(st, en));

    //fclose(stdin);
    //fclose(stdout);
}

至此便完成了Dinic算法。


實測效率
仍是在luogu的P3376 【模板】網絡最大流中測評。

結果:耗時/內存 429ms , 56949kb
相比於EK算法的:耗時/內存 392ms , 56988kb 彷佛要慢上一些,實際上關於網絡流算法的時間複雜度是玄學,只能獲得上限(最壞狀況)沒法獲得每次實際的複雜度。由於是BFS+DFS,次數,節點連通狀況咱們都沒法計算,故時間複雜度意義不大,實測結果與具體數據有關。
一般不會超時,由於面對極坑的數據(例如真的到了N^2M的狀況),奈何出題人再優化他的標程也不可能按時跑出來(100000* 100000* 10000,你行你來…),會改數據的…
因此在使用這種算法的時候仍是別考慮在這上面優化了千萬不要忘記讀入優化,比你優化別的半天強多了)。

至此Dinic算法的分析便結束了。
箜瑟_qi 2017.04.09 20:42

相關文章
相關標籤/搜索