【原創】我的KM算法詳解


0.二分圖

二分圖的概念

二分圖又稱作二部圖,是圖論中的一種特殊模型。
設G=(V, E)是一個無向圖。如果頂點集V可分割為兩個互不相交的子集X和Y,並且圖中每條邊連接的兩個頂點一個在X中,另一個在Y中,則稱圖G為二分圖。
可以得到線上的driver與order之間的匹配關系既是一個二分圖。

二分圖的判定

無向圖G為二分圖的充分必要條件是,G至少有兩個頂點,且其所有回路的長度均為偶數。
判斷無向連通圖是不是二分圖,可以使用深度優先遍歷算法(又名交叉染色法)。
下面着重介紹下交叉染色法的定義與原理
首先任意取出一個頂點進行染色,和該節點相鄰的點有三種情況:
       1.如果節點沒有染過色,就染上與它相反的顏色,推入隊列,
       2.如果節點染過色且相反,忽視掉,
       3.如果節點染過色且與父節點相同,證明不是二分圖,return

二分圖博客推薦

交叉染色法博客推薦: 交叉染色法判斷二分圖
另外附上二分圖的性質博客: 二分圖的一些性質

1.KM算法初步

KM算法全稱是Kuhn-Munkras,是這兩個人在1957年提出的,有趣的是,匈牙利算法是在1965年提出的。 

增廣路徑

增廣路徑定義:
若P是圖G中一條連通兩個未匹配頂點的路徑,並且屬於M的邊和不屬於M的邊(即已匹配和待匹配的邊)在P上交替出現,則稱P為相對於M的一條增廣路徑
(舉例來說,有A、B集合,增廣路由A中一個點通向B中一個點,再由B中這個點通向A中一個點……交替進行)
增廣路徑有如下特性: 
1. 有奇數條邊 
2. 起點在二分圖的X邊,終點在二分圖的Y邊 
3. 路徑上的點一定是一個在X邊,一個在Y邊,交錯出現。 
4. 整條路徑上沒有重復的點 
5. 起點和終點都是目前還沒有配對的點,其他的點都已經出現在匹配子圖中 
6. 路徑上的所有第奇數條邊都是目前還沒有進入目前的匹配子圖的邊,而所有第偶數條邊都已經進入目前的匹配子圖。奇數邊比偶數邊多一條邊 
7. 於是當我們把所有第奇數條邊都加到匹配子圖並把條偶數條邊都刪除,匹配數增加了1.
 
例如下圖,藍色的是當前的匹配子圖,紅色表示未匹配的路徑,目前只有邊x0y0,然后通過x1找到了增廣路徑:x1y0->y0x0->x0y2 

 

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

匈牙利算法

匈牙利算法,用於求二分圖的最大匹配。何為最大匹配?假設每條邊有權值,那么一定會存在一個最大權值的匹配情況。

匈牙利算法步驟

算法根據一定的規則選擇二分圖的邊加入匹配子圖中,其基本模式為:
1.初始化匹配子圖為空 
2.While 找得到增廣路徑 
3.Do 把增廣路徑添加到匹配子圖中

匈牙利算法博客推薦

KM深度優先遍歷算法,其中附帶講解圖可參考博客: 趣寫算法系列之–匈牙利算法
最大匹配的講解博客: 匈牙利算法(二分圖)

KM算法

KM算法,用於求二分圖匹配的最佳匹配。何為最佳匹配?就是帶權二分圖的權值最大的完備匹配稱為最佳匹配。 那么何為完備匹配?X部中的每一個頂點都與Y部中的一個頂點匹配,或者Y部中的每一個頂點也與X部中的一個頂點匹配,則該匹配為完備匹配。

KM算法步驟

其算法步驟如下:
1.用鄰接矩陣(或其他方法也行啦)來儲存圖,注意:如果只是想求最大權值匹配而不要求是完全匹配的話,請把各個不相連的邊的權值設置為0。
2.運用貪心算法初始化標桿。
3.運用匈牙利算法找到完備匹配。
4.如果找不到,則通過修改標桿,增加一些邊。
5.重復3,4的步驟,直到完全匹配時可結束。

KM算法標桿(又名頂標)的引入

二分圖最佳匹配還是二分圖匹配,所以跟和匈牙利算法思路差不多。
二分圖是特殊的網絡流,最佳匹配相當於求最大(小)費用最大流,所以FF算法(全名Ford-Fulkerson算法)也能實現。
  • 所以我們可以把這匈牙利算法和FF算法結合起來。這就是KM算法的思路了:盡量找最大的邊進行連邊,如果不能則換一條較大的。
    • FF算法里面,我們每次是找最長(短)路進行通流,所以二分圖匹配里面我們也按照FF算法找最大邊進行連邊!
    • 但是遇到某個點被匹配了兩次怎么辦?那就用匈牙利算法進行更改匹配!
  • 所以,根據KM算法的思路,我們一開始要對邊權值最大的進行連線。
  • 那問題就來了,我們如何讓計算機知道該點對應的權值最大的邊是哪一條?或許我們可以通過某種方式記錄邊的另一端點,但是呢,后面還要涉及改邊,又要記錄邊權值總和,而這個記錄端點方法似乎有點麻煩。
    • 於是KM采用了一種十分巧妙的辦法(也是KM算法思想的精髓):添加標桿(頂標)
添加標桿(頂標)流程:
  • 我們對左邊每個點Xi和右邊每個點Yi添加標桿Cx和Cy。其中我們要滿足Cx+Cy>=w[x][y](w[x][y]即為點Xi、Yi之間的邊權值)
  • 對於一開始的初始化,我們對於每個點分別進行如下操作:Cx=max(w[x][y]);  Cy=0;
添加頂標之前的二分圖:
                
添加頂標之后的二分圖:
 

KM流程詳解

  • 初始化可行頂標的值 (設定lx,ly的初始值)
  • 用匈牙利算法尋找相等子圖的完備匹配
  • 若未找到增廣路則修改可行頂標的值
  • 重復(2)(3)直到找到相等子圖的完備匹配為止
使用上圖的例子,采用匈牙利算法進行連邊操作,將最大邊進行連線。所以原來判斷是否有邊的條件w[x][y]==0換成了 Cx+Cy==w[x][y]
  • 於是乎我們連了AD,形成一個新的二分圖(我們下面叫它二分子圖好了)
  • 接下來就尷尬了,計算機接下來要連B點的BD,但是D點已經和A點連了,怎么辦呢???
    • 根據匈牙利算法,我們做的是將A點與其他點進行連線,但此時的子圖里“不存在”與A點相連的其他邊,怎么辦呢???
      • 為此,我們就需要加上這些邊!很明顯,我們添邊,自然要加上不在子圖中邊權最大的邊,也就是和子圖里這個邊權值差最小的邊。
        • 於是,我們再一度引入了一變量d,d=min{Cx[i]+Cy[j]-w[i][j]},其中,在這個題目里Cx[i]指的是A的標桿,Cy[j]是除D點(即已連點)以外的點的標桿。
          • 隨后,對於原先存在於子圖的邊AD,我們將A的標桿Cx[i]減去d,D的標桿Cy[d]加上d。
            • 這樣,這就保證了原先存在AD邊保留在了子圖中,並且把不在子圖的最大權值的與A點相連的邊AE添加到了子圖。
            • 因為計算機判斷一條邊是否在該子圖的條件是其兩端的頂點的標桿滿足Cx+Cy==w[x][y]
              • 對於原先的邊,我們對左端點的標桿減去了d,對右端點的標桿加上了d,所以最終的結果還是不變,仍然是w[x][y]。
              • 對於我們要添加的邊,我們對於左端點減去了d,即Cx[i]=Cx[i]-d;為方便表示我們把更改后的的Cx[i]視為Cz[i],即Cz[i]=Cx[i]-d;
                • 因為Cz[i]=Cx[i]-d;d=Cx[i]+Cy[j]-w[i][j];
                • 把d代入左式可得Cz[i]=Cx[i]-(Cx[i]+Cy[j]-w[i][j]);
                • 化簡得Cz[i]+Cy[j]=w[i][j];
                • 滿足了要求!即添加了新的邊。
    • 重復進行上述流程。(匈牙利算法以及FF算法的結合) 

KM算法博客推薦

頂標內容講的很好: KM算法
松弛度內容講的比較好: 二分圖的最佳完美匹配——KM算法
匈牙利算法和FF算法結合得到KM算法講的很詳細: 二分圖匹配之最佳匹配——KM算法
最佳講解博客推薦: 我對KM算法的理解

2.DFS版本的KM算法

/*==================================================*\
 |  二分圖匹配(匈牙利算法DFS 實現)
 |  INIT: graph[][]鄰接矩陣;
 |  CALL: res =  dfsHungarian ();
 |  優點:實現簡潔容易理解,適用於稠密圖,DFS 找增廣路快。
 |  找一條增廣路的復雜度為O(E),最多找V條增廣路,故時間復雜度為O(VE)
 |  算法簡述:
 |  從二分圖中找出一條路徑來,讓路徑的起點和終點都是還沒有匹配過的點,
 |  並且路徑經過的連線是一條沒被匹配、一條已經匹配過,再下一條又沒匹配這樣交替地出現。
 |  找到這樣的路徑后,顯然路徑里沒被匹配的連線比已經匹配了的連線多一條,
 |  於是修改匹配圖,把路徑里所有匹配過的連線去掉匹配關系,把沒有匹配的連線變成匹配的。
 |  這樣匹配數就比原來多1個。不斷執行上述操作,直到找不到這樣的路徑為止。
 \*==================================================*/
#include<iostream> 
#include<memory.h> 
using namespace std; 
   
#define MAXN 10 
int graph[MAXN][MAXN]; 
int match[MAXN]; 
int visitX[MAXN], visitY[MAXN]; 
int nx, ny; 
   
bool findPath( int u ) 
{ 
    visitX[u] = 1; 
    for( int v=0; v<ny; v++ ) 
    { 
        if( !visitY[v] && graph[u][v] ) 
        { 
            visitY[v] = 1; 
            if( match[v] == -1 //第一次,用到了短路計算,否則findPath(-1)會出問題
                    || findPath(match[v]) )//這里就表示深度優先遍歷 不撞南山頭不回 不見黃河心不死
            { 
                match[v] = u; 
                return true; 
            } 
        } 
    } 
    return false; 
} 
   
int dfsHungarian() 
{ 
    int res = 0; 
    memset( match, -1, sizeof(match) ); 
    for( int i=0; i<nx; i++ ) 
    { 
        memset( visitX, 0, sizeof(visitX) ); 
        memset( visitY, 0, sizeof(visitY) ); 
        if( findPath(i) ) 
            res++; 
    } 
    return res; 
}

3.BFS版本的KM算法

/*==================================================*\
  |  二分圖匹配(匈牙利算法BFS 實現)
  |  INIT: graph[][]鄰接矩陣;
  |  CALL: res =  bfsHungarian ();
  |  優點:適用於稀疏二分圖,邊較少,增廣路較短。
  |  匈牙利算法的理論復雜度是O(VE)
  \*==================================================*/
#include<iostream> 
#include<memory.h> 
using namespace std; 
   
#define MAXN 10 
int graph[MAXN][MAXN]; 
//在bfs中,增廣路徑的搜索是一層一層展開的,所以必須通過prevX來記錄上一層的頂點 
//chkY用於標記某個Y頂點是否被目前的X頂點訪問嘗試過。 
int matchX[MAXN], matchY[MAXN], prevX[MAXN], chkY[MAXN]; 
int queue[MAXN]; 
int nx, ny; 
   
int bfsHungarian() 
{ 
    int res = 0; 
    int qs, qe; 
    memset( matchX, -1, sizeof(matchX) ); 
    memset( matchY, -1, sizeof(matchY) ); 
    memset( chkY, -1, sizeof(chkY) ); 
   
    for( int i=0; i<nx; i++ ) 
    { 
        if( matchX[i] == -1 )   //如果該X頂點未找到匹配點,將其放入隊列。 
        { 
            qs = qe = 0; 
            queue[qe++] = i; 
            prevX[i] = -1;  //並且標記,它是路徑的起點 
            bool flag = 0; 
   
            while( qs<qe && !flag ) 
            { 
                int u = queue[qs]; 
                for( int v=0; v<ny&&!flag; v++ ) 
                { 
                    if( graph[u][v] && chkY[v]!=i ) //如果該節點與u有邊且未被訪問過 
                    { 
                        chkY[v] = i;    //標記且將它的前一個頂點放入隊列中,也就是下次可能嘗試這個頂點看能否為它找到新的節點 
                        queue[qe++] = matchY[v]; 
                        if( matchY[v] >= 0 ) 
                            prevX[matchY[v]] = u; 
                        else    //到達了增廣路徑的最后一站 
                        { 
                            flag = 1; 
                            int d=u, e=v; 
                            while( d!=-1 )  //一路通過prevX找到路徑的起點 
                            { 
                                int t = matchX[d]; 
                                matchX[d] = e; 
                                matchY[e] = d; 
                                d = prevX[d]; 
                                e = t; 
                            } 
                        } 
                    } 
                } 
                qs++; 
            } 
            if( matchX[i] != -1 ) 
                res++; 
        } 
    } 
    return res; 
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM