【原創】個人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; 
}
相關文章
相關標籤/搜索