淺談KM算法

出處:http://blog.sina.com.cn/s/blog_6cf509db0100uy5n.html

因爲是初學教程,所以我會盡量避免繁雜的數學公式和證明。也儘量給出了較爲完整的代碼。

本文的目標羣體是網絡流的初學者,尤其是看了各種NB的教程也沒看懂怎麼求最大流的小盆友們。本文的目的是,解釋基本的網絡流模型,最基礎的最大流求法,即bfs找增廣路法,也就是EK法,全名是Edmond-Karp,其實我倒是覺得記一下算法的全名和來歷可以不時的拿出來裝一裝。

    比如說這個,EK算法首先由俄羅斯科學家Dinic在1970年提出,沒錯,就是dinic算法的創始人,實際上他提出的也正是dinic算法,在EK的基礎上加入了層次優化,這個我們以後再說,1972年Jack Edmonds和Richard Karp發表了沒有層次優化的EK算法。但實際上他們是比1790年更早的時候就獨立弄出來了。

    你看,研究一下歷史也是很有趣的。

    扯遠了,首先來看一下基本的網絡流最大流模型。

    有n個點,有m條有向邊,有一個點很特殊,只出不進,叫做源點,通常規定爲1號點。另一個點也很特殊,只進不出,叫做匯點,通常規定爲n號點。每條有向邊上有兩個量,容量和流量,從i到j的容量通常用c[I,j]表示,流量則通常是f[I,j]。通常可以把這些邊想象成道路,流量就是這條道路的車流量,容量就是道路可承受的最大的車流量。很顯然的,流量<=容量。而對於每個不是源點和匯點的點來說,可以類比的想象成沒有存儲功能的貨物的中轉站,所有”進入”他們的流量和等於所有從他本身”出去”的流量。

    把源點比作工廠的話,問題就是求從工廠最大可以發出多少貨物,是不至於超過道路的容量限制,也就是,最大流。

    比如這個圖。每條邊旁邊的數字表示它的容量。

 

最大流模板【EdmondsKarp算法,簡稱EK算法,O(m^2n)】



    下面我們來考慮如何求最大流。

    首先,假如所有邊上的流量都沒有超過容量(不大於容量),那麼就把這一組流量,或者說,這個流,稱爲一個可行流。一個最簡單的例子就是,零流,即所有的流量都是0的流。

我們就從這個零流開始考慮,假如有這麼一條路,這條路從源點開始一直一段一段的連到了匯點,並且,這條路上的每一段都滿足流量<容量,注意,是嚴格的<,而不是<=。那麼,我們一定能找到這條路上的每一段的(容量-流量)的值當中的最小值delta。我們把這條路上每一段的流量都加上這個delta,一定可以保證這個流依然是可行流,這是顯然的。

    這樣我們就得到了一個更大的流,他的流量是之前的流量+delta,而這條路就叫做增廣路。

    我們不斷地從起點開始尋找增廣路,每次都對其進行增廣,直到源點和匯點不連通,也就是找不到增廣路爲止。當找不到增廣路的時候,當前的流量就是最大流,這個結論非常重要。

尋找增廣路的時候我們可以簡單的從源點開始做bfs,並不斷修改這條路上的delta量,直到找到源點或者找不到增廣路。

這裏要先補充一點,在程序實現的時候,我們通常只是用一個c數組來記錄容量,而不記錄流量,當流量+1的時候,我們可以通過容量-1來實現,以方便程序的實現。

 

Bfs過程的半僞代碼:下面另給一個C++版的模板

int BFS()
{
    int i,j,k,v,u;
    memset(pre,-1,sizeof(pre));
    for(i=1;i<=n;++i)flow[i]=max_int;
    queue<int>que;
    pre[start]=0;
    que.push(start);
    while(!que.empty())
    {
        v=que.front();
        que.pop();
        for(i=1;i<=n;++i)
        {
            u=i;
            if(u==start||pre[u]!=-1||map[v][u]==0)continue;
            pre[u]=v;
            flow[u]=MIN(flow[v],map[v][u]);
            que.push(u);
        }
    }
    if(flow[end]==max_int)return -1;
    return flow[end];
}

但事實上並沒有這麼簡單,上面所說的增廣路還不完整,比如說下面這個網絡流模型。

最大流模板【EdmondsKarp算法,簡稱EK算法,O(m^2n)】


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

最大流模板【EdmondsKarp算法,簡稱EK算法,O(m^2n)】


這時候(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這條增廣路之後,把容量修改成如下

最大流模板【EdmondsKarp算法,簡稱EK算法,O(m^2n)】


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

最大流模板【EdmondsKarp算法,簡稱EK算法,O(m^2n)】

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

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

這就是這個算法的精華部分,利用反向邊,使程序有了一個後悔和改正的機會。而這個算法和我剛纔給出的代碼相比只多了一句話而已。
#include<iostream>
#include<queue>
using namespace std;
const int maxn=205;
const int inf=0x7fffffff;

int r[maxn][maxn]; //殘留網絡,初始化爲原圖
bool visit[maxn];
int pre[maxn];
int m,n;

bool bfs(int s,int t)  //尋找一條從s到t的增廣路,若找到返回true
{
    int p;
    queue<int > q;
    memset(pre,-1,sizeof(pre));
    memset(visit,false,sizeof(visit));

    pre[s]=s;
    visit[s]=true;
    q.push(s);
    while(!q.empty())
    {
        p=q.front();
        q.pop();
        for(int i=1;i<=n;i++)
        {
            if(r[p][i]>0&&!visit[i])
            {
                pre[i]=p;
                visit[i]=true;
                if(i==t) return true;
                q.push(i);
            }
        }
    }
    return false;
}

int EdmondsKarp(int s,int t)
{
   int flow=0,d,i;
   while(bfs(s,t))
   {
       d=inf;
       for(i=t;i!=s;i=pre[i])
           d=d<r[pre[i]][i]? d:r[pre[i]][i];
       for(i=t;i!=s;i=pre[i])
       {
           r[pre[i]][i]-=d;
           r[i][pre[i]]+=d;
       }
       flow+=d;
   }
   return flow;
}


int main()
{
    while(scanf("%d%d",&m,&n)!=EOF)
    {
        int u,v,w;
        memset(r,0,sizeof(r));///
        for(int i=0;i<m;i++)
        {
            scanf("%d%d%d",&u,&v,&w);
            r[u][v]+=w;
        }
        printf("%d\n",EdmondsKarp(1,n));
    }
    return 0;
}

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