1. 程式人生 > >【最大流之ek演算法】HDU1532 求最大流

【最大流之ek演算法】HDU1532 求最大流



本來是繼續加強最短路的訓練,但是遇到了一個最短路 + 最大流的問題,最大流什麼鬼,昨天+今天學習了一下,應該對ek演算法有所瞭解,憑藉學習後的印象,自己完成並ac了這個最大流的模板題

題目大意:都是圖論,只是這個圖給你的關係是網路關係,就是從s到t的路上,你運送的東西的量必須滿足所有路徑的限制,而題目給出的就是限制,我們用一個二維陣列c儲存i到j這一條邊的總限制,注意的是,初始化c陣列為0,然後增加限制,而不是賦值,因為可能有多條路,但管他幾條路呢,只要能運過去走那條都行,所以合併增值

int main()
{
    while(~scanf("%d%d",&m,&n))
    {
        int a,b,x;
        memset(c,0,sizeof(c));//最初肯定是不能通過流量
        while(m--)
        {
            scanf("%d%d%d",&a,&b,&x);
            c[a][b] += x;//多條路的情況合併為其流量總限制即可
        }
        int ret = ek(1,n);
        printf("%d\n",ret);
    }
    return 0;
}

 接下來是ek演算法,他的主要思想就是反向邊的維護,因為讓求最大流只用bfs,dfs求出的所有結果不一定是最優最大的結果,但是我們可以先用bfs進行嘗試,但是嘗試完這一條路後,我們根據這條路建立一個反向的路,反向路可行表示的結果就是,這一條路被抵消了

引入一下圖文解釋

下面是所有最大流演算法的精華部分:引入反向邊

為什麼要有反向邊呢?

 

我們第一次找到了1-2-3-4這條增廣路,這條路上的delta值顯然是1。於是我們修改後得到了下面這個流。(圖中的數字是容量)

這時候(1,2)和(3,4)邊上的流量都等於容量了,我們再也找不到其他的增廣路了,當前的流量是1。

但這個答案明顯不是最大流,因為我們可以同時走1-2-4和1-3-4,這樣可以得到流量為2的流。

那麼我們剛剛的演算法問題在哪裡呢?問題就在於我們沒有給程式一個”後悔”的機會,應該有一個不走(2-3-4)而改走(2-4)的機制。那麼如何解決這個問題呢?回溯搜尋嗎?那麼我們的效率就上升到指數級了。

而這個演算法神奇的利用了一個叫做反向邊的概念來解決這個問題。即每條邊(I,j)都有一條反向邊(j,i),反向邊也同樣有它的容量。

我們直接來看它是如何解決的:

在第一次找到增廣路之後,在把路上每一段的容量減少delta的同時,也把每一段上的反方向的容量增加delta。即在Dec(c[x,y],delta)的同時,inc(c[y,x],delta)

我們來看剛才的例子,在找到1-2-3-4這條增廣路之後,把容量修改成如下

這時再找增廣路的時候,就會找到1-3-2-4這條可增廣量,即delta值為1的可增廣路。將這條路增廣之後,得到了最大流2。

那麼,這麼做為什麼會是對的呢?我來通俗的解釋一下吧。

事實上,當我們第二次的增廣路走3-2這條反向邊的時候,就相當於把2-3這條正向邊已經是用了的流量給”退”了回去,不走2-3這條路,而改走從2點出發的其他的路也就是2-4。(有人問如果這裡沒有2-4怎麼辦,這時假如沒有2-4這條路的話,最終這條增廣路也不會存在,因為他根本不能走到匯點)同時本來在3-4上的流量由1-3-4這條路來”接管”。而最終2-3這條路正向流量1,反向流量1,等於沒有流量。

這就是這個演算法的精華部分,利用反向邊,使程式有了一個後悔和改正的機會

第一個隆重登場的演算法是 EK(Edmond—Karp)演算法

其實你看看3為什麼要走那條反向路,是因為第一條路1 - 2 - 4把3 - 4 佔用了,那麼我只能1 - 3  - 2走它的反向路,並嘗試走到終點4,如果能走到終點4,那麼就代表最終可行線路是1 - 2 - 4和 1 - 3 - 4 ,很想匈牙利的二分圖匹配啊,我覺得這麼可以理解,就是1 - 3一旦走了反向路就代表他在為1 - 2 尋找另一條不是1 - 2 - 3 - 4 的路線,如果找到了 1 - 2 - 4確定可以走的通,而原來的正向路也能保證1 3 4可以走的通,所以路線就能有化成1 2 4 和1 3 4,如果找不到就不會有任何變化(真的很閒匈牙利啊~~--.--)

所以我們要些bfs,是用來嘗試連線1 - n ,並且連線成功返回該線路的最大流,連線失敗返回一個flag傳到ek演算法裡

所以在bfs中我們有一個f【i】陣列表示從點1到i的最大流,和pre陣列,記錄該線路的前驅,為ek演算法建立反向邊做準備

int bfs(int s,int t)
{
    memset(pre,-1,sizeof(pre));
    f[s] = inf;
    queue<int> q;
    while(q.size())q.pop();
    q.push(s);
    while(q.size())
    {
        int now = q.front();q.pop();
        for(int i = 1;i <= n;i++)
        {
            if(c[now][i] > 0 && pre[i] == -1)
            {
                pre[i] = now;
                f[i] = min(f[now],c[now][i]);
                q.push(i);
            }
        }
    }
    if(pre[t] == -1)return -1;
    return f[t];

}

 上面的程式碼:pre陣列還被用來做一個訪問標記vis陣列

然後就是ek演算法建立反向邊的問題了,其實就是利用bfs傳回的值,對二維陣列c進行的增值和減值的操作

int ek(int s,int t)
{
    int ret = 0;
    while(1)
    {
        int delta = bfs(s,t);
        if(delta == -1)break;
        int now = t;
        while(now != s)
        {
            c[pre[now]][now] -= delta;
            c[now][pre[now]] += delta;
            now = pre[now];
        }
        ret += delta;
    }
    return ret;
}

————————————————————————————————————————————————————————————

加油!