1. 程式人生 > >Kuhn-Munkres演算法(二分圖最大權匹配)

Kuhn-Munkres演算法(二分圖最大權匹配)

二分圖最優匹配:

接下來就是二分圖最優匹配的km演算法了

km演算法理解起來著實很困難,我其實只能照著程式碼講,不然根本講不明白。不過聽一個學長說要理解思想而不是程式碼。。。那就試著空講一下吧。

一般對KM演算法的描述,基本上可以概括成以下幾個步驟: 
(1) 初始化可行標杆 
(2) 用匈牙利演算法尋找完備匹配 
(3) 若未找到完備匹配則修改可行標杆 
(4) 重複(2)(3)直到找到相等子圖的完備匹配 

關於該演算法的流程及實施,網上有很多介紹,基本上都是圍繞可行標杆如何修改而進行的討論,至於原理並沒有給出深入的探討。 

KM演算法是用於尋找帶權二分圖最佳匹配的演算法。 

二分圖是這樣一種圖:所有頂點可以分成兩個集:X和Y,其中X和Y中的任意兩個在同一個集中的點都不相連,而來自X集的頂點與來自Y集的頂點有連線。當這些連線被賦於一定的權重時,這樣的二分圖便是帶權二分圖。 



二分圖匹配是指求出一組邊,其中的頂點分別在兩個集合中,且任意兩條邊都沒有相同的頂點,這組邊叫做二分圖的匹配,而所能得到的最大的邊的個數,叫做二分圖的最大匹配。 

我們也可以換個角度看二分圖的最大匹配,即二分圖的每條邊的預設權重為1,我們求到的二分圖的最大匹配的權重最大。對於帶權二分圖,其邊有大於0的權重,找到一組匹配,使其權重最大,即為帶權二分圖的最佳匹配。 

匈牙利演算法一般用於尋找二分圖的最大匹配。演算法根據一定的規則選擇二分圖的邊加入匹配子圖中,其基本模式為: 

初始化匹配子圖為空 
While 找得到增廣路徑 
Do 把增廣路徑新增到匹配子圖中 

增廣路徑有如下特性: 
1. 有奇數條邊 
2. 起點在二分圖的X邊,終點在二分圖的Y邊 

3. 路徑上的點一定是一個在X邊,一個在Y邊,交錯出現。 
4. 整條路徑上沒有重複的點 
5. 起點和終點都是目前還沒有配對的點,其他的點都已經出現在匹配子圖中 
6. 路徑上的所有第奇數條邊都是目前還沒有進入目前的匹配子圖的邊,而所有第偶數條邊都已經進入目前的匹配子圖。奇數邊比偶數邊多一條邊 
7. 於是當我們把所有第奇數條邊都加到匹配子圖並把條偶數條邊都刪除,匹配數增加了1. 

例如下圖,藍色的是當前的匹配子圖,目前只有邊x0y0,然後通過x1找到了增廣路徑:x1y0->y0x0->x0y2 



其中第奇數第邊x1y0和x0y2不在當前的匹配子圖中,而第偶數條邊x0y0在匹配子圖中,通過新增x1y0和x0y2到匹配子圖並刪除x0y0,使得匹配數由1增加到了2。每找到一條增廣路徑,通過新增刪除邊,我們總是能使匹配數加1. 


增廣路徑有兩種尋徑方法,一個是深搜,一個是寬搜。例如從x2出發尋找增廣路徑,如果是深搜,x2找到y0匹配,但發現y0已經被x1匹配了,於是就深入到x1,去為x1找新的匹配節點,結果發現x1沒有其他的匹配節點,於是匹配失敗,x2接著找y1,發現y1可以匹配,於是就找到了新的增廣路徑。如果是寬搜,x1找到y0節點的時候,由於不能馬上得到一個合法的匹配,於是將它做為候選項放入佇列中,並接著找y1,由於y1已經匹配,於是匹配成功返回了。相對來說,深搜要容易理解些,其棧可以由遞迴過程來維護,而寬搜則需要自己維護一個佇列,並對一路過來的路線自己做標記,實現起來比較麻煩。 

對於帶權重的二分圖來說,我們可以把它看成一個所有X集合的頂點到所有Y集合的頂點均有邊的二分圖(把原來沒有的邊新增入二分圖,權重為0即可),也就是說它必定存在完備匹配(即其匹配數為min(|X|,|Y|))。為了使權重達到最大,我們實際上是通過貪心演算法來選邊,形成一個新的二分圖(我們下面叫它二分子圖好了),並在該二分圖的基礎上尋找最大匹配,當該最大匹配為完備匹配時,我們可以確定該匹配為最佳匹配。(在這裡我們如此定義最大匹配:匹配邊數最多的匹配和最佳匹配:匹配邊的權重和最大的匹配。) 

貪心演算法總是將最優的邊優先加入二分子圖,該最優的邊將對當前的匹配子圖帶來最大的貢獻,貢獻的衡量是通過標杆來實現的。下面我們將通過一個例項來解釋這個過程。 

有帶權二分圖: 


演算法把權重轉換成標杆,X集跟Y集的每個頂點各有一個標杆值,初始情況下權重全部放在X集上。由於每個頂點都將至少會有一個匹配點,貪心演算法必然優先選擇該頂點上權重最大的邊(最理想的情況下,這些邊正好沒有交點,於是我們自然得到了最佳匹配)。最初的二分子圖為:(可以看到初始化時X標杆為該頂點上的最大權重,而Y標杆為0) 


從X0找增廣路徑,找到X0Y4;從X1找不到增廣路徑,也就是說,必須往二分子圖裡邊新增新的邊,使得X1能找到它的匹配,同時使權重總和新增最大。由於X1通往Y4而Y4已經被X0匹配,所以有兩種可能,一個是為X0找一個新的匹配點並把Y4讓給X1,或者是為X1找一個新的匹配點,現在我們將要看到標杆的作用了。根據傳統的演算法描述,能夠進入二分子圖的邊的條件為L(x)+L(y)<=weight(xy)。當找不到增廣路徑時,對於搜尋過的路徑上的XY點,設該路徑上的X頂點集為S,Y頂點集為T,對所有在S中的點xi及不在T中的點yj,計算d=min{(L(xi)+L(yj)-weight(xiyj))},從S集中的X標杆中減去d,並將其加入到T集中的Y的標杆中,由於S集中的X標杆減少了,而不在T中的Y標杆不變,相當於這兩個集合中的L(x)+L(y)變小了,也就是,有新的邊可以加入二分子圖了。從貪心選邊的角度看,我們可以為X0選擇新的邊而拋棄原先的二分子圖中的匹配邊,也可以為X1選擇新的邊而拋棄原先的二分子圖中的匹配邊,因為我們不能同時選擇X0Y4和X1Y4,因為這是一個不合法匹配,這個時候,d=min{(L(xi)+L(yj)-weight(xiyj))}的意義就在於,我們選擇一條新的邊,這條邊將被加入匹配子圖中使得匹配合法,選擇這條邊形成的匹配子圖,將比原先的匹配子圖加上這條非法邊組成的非法匹配子圖的權重和(如果它是合法的,它將是最大的)小最少,即權重最大了。好繞口的。用數學的方式表達,設原先的不合法匹配(它的權重最大,因為我們總是從權重最大的邊找起的)的權重為W,新的合法匹配為W’,d為min{W-W’i}。在這個例子中,S={X0,
X1},Y={Y4},求出最小值d=L(X1)+L(Y0)-weight(X1Y0)=2,得到新的二分子圖: 



重新為X1尋找增廣路徑,找到X1Y0,可以看到新的匹配子圖的權重為9+6=15,比原先的不合法的匹配的權重9+8=17正好少d=2。 
接下來從X2出發找不到增廣路徑,其走過的路徑如藍色的路線所示。形成的非法匹配子圖:X0Y4,X1Y0及X2Y0的權重和為22。在這條路徑上,只要為S={X0,X1,X2}中的任意一個頂點找到新的匹配,就可以解決這個問題,於是又開始求d。 
d=L(X0)+L(Y2)-weight(X0Y2)=L(X2)+L(Y1)-weight(X2Y1)=1. 
新的二分子圖為: 



重新為X2尋找增廣路徑,如果我們使用的是深搜,會得到路徑:X2Y0->Y0X1->X1Y4->Y4X0->X0Y2,即奇數條邊而刪除偶數條邊,新的匹配子圖中由這幾個頂點得到的新的權重為21;如果使用的是寬搜,會得到路徑X2Y1,另上原先的兩條匹配邊,權重為21。假設我們使用的是寬搜,得到的新的匹配子圖為: 


接下來依次類推,直到為X4找到一個匹配點。 

KM演算法的最大特點在於利用標杆和權重來生成一個二分子圖,在該二分子圖上面找最大匹配,而且,當些僅當找到完備匹配,才能得到最佳匹配。標杆和權重的作用在於限制新邊的加入,使得加入的新邊總是能為子圖新增匹配數,同時又令權重和得到最大的提高。


模板
#include<iostream>
#include<algorithm>
#include<cstring>
#include<stdio.h>
using namespace std;
const int maxn = 305;
const int inf = 0x3f3f3f3f;
int map[maxn][maxn];

int cx[maxn], cy[maxn];
int dx[maxn], dy[maxn];
int lx[maxn], ly[maxn];
int N;

bool find(int u)
{
    dx[u] = 1;
    for (int i = 1;i <= N;i++)if (!dy[i] && lx[u] + ly[i] == map[u][i])
    {
        dy[i] = 1;
        if (cy[i] == -1 || find(cy[i]))
        {
            cx[u] = i;
            cy[i] = u;
            return 1;
        }
    }
    return 0;
}

int KM()
{
    memset(cx, -1, sizeof(cx));
    memset(cy, -1, sizeof(cy));
    for (int i = 1;i <= N;i++)
    {
        lx[i] = 0;ly[i] = 0;
        for (int j = 1;j <= N;j++)
            lx[i] = max(lx[i], map[i][j]);
    }
    for (int u = 1;u <= N;u++)
    {
        while (1)
        {
            memset(dx, 0, sizeof(dx));
            memset(dy, 0, sizeof(dy));
            if (find(u))break;
            int inc = inf;
            for (int i = 1;i <= N;i++)if (dx[i])
                for (int j = 1;j <= N;j++)if (!dy[j])
                    inc = min(inc, lx[i] + ly[j] - map[i][j]);
            for (int i = 1;i <= N;i++)
            {
                if (dx[i])lx[i] -= inc;
                if (dy[i])ly[i] += inc;
            }

        }
    }
    int ans = 0;
    for (int i = 1;i <= N;i++)
        ans += map[i][cx[i]];
    return ans;
}

int main()
{

    while (~scanf("%d", &N))
    {
        for (int i = 1;i <= N;i++)
            for (int j = 1;j <= N;j++)
                scanf("%d", &map[i][j]);
        printf("%d\n", KM());
    }
    return 0;
}