KM(Kuhn-Munkres)算法求帶權二分圖的最佳匹配
相關概念
這個算法個人覺得一開始時有點難以理解它的一些概念,特別是新定義出來的,因為不知道是干嘛用的。但是,在了解了算法的執行過程和原理后,這些概念的意義和背后的作用就漸漸的顯示出來了。因此,先暫時把相關概念列出來,看看,有個大概印象就好,等到了解了算法的流程后,在看原理中會有這些概念,那個時候回來細看就好了。
完備匹配:定義 設G=<V1,V2,E>為二部圖,|V1|≤|V2|,M為G中一個最大匹配,且|M|=|V1|,則稱M為V1到V2的完備匹配。 也就是說把一個集合中的點全部匹配到另一個集合中。(之所以先介紹這個概念,是因為當時看了很久,好多概念里都有提到完備匹配,但是我並不知道什么是完備匹配。)在上述定義中,若|V2|=|V1|,則完備匹配即為完美匹配,若|V1|<|V2|,則完備匹配為G中最大匹配。
二分圖最優匹配:對於二分圖的每條邊都有一個權(非負),要求一種完備匹配方案,使得所有匹配邊的權和最大,記做最優完備匹配。(特殊的,當所有邊的權為1時,就是最大完備匹配問題)
二分圖帶權匹配與最優匹配:什么是二分圖的帶權匹配?二分圖的帶權匹配就是求出一個匹配集合,使得集合中邊的權值之和最大或最小。而二分圖的最優匹配則一定為完備匹配,在此基礎上,才要求匹配的邊權值之和最大或最小。二分圖的帶權匹配與最優匹配不等價,也不互相包含。
以下是一些轉換的思路:
KM算法是求最大權完備匹配,如果要求最小權完備匹配怎么辦?方法很簡單,只需將所有的邊權值取其相反數,求最大權完備匹配,匹配的值再取相反數即可。
KM算法的運行要求是必須存在一個完備匹配,如果求一個最大權匹配(不一定完備)該如何辦?依然很簡單,把不存在的邊權值賦為0。
KM算法求得的最大權匹配是邊權值和最大,如果我想要邊權之積最大,又怎樣轉化?還是不難辦到,每條邊權取自然對數,然后求最大和權匹配,求得的結果a再算出e^a就是最大積匹配。至於精度問題則沒有更好的辦法了。
下面是算法需要而建立的概念:
頂標:每個節點與另一個集合中節點之間的最大權值
可行頂標:對於原圖中的任意一個結點,給定一個函數L(node)求出結點的頂標值。我們用數組lx(x)記錄集合X中的結點頂標值,用數組ly(y)記錄集合Y中的結點頂標值。 並且,對於原圖中任意一條邊\(edge(x,y)\),都滿足\(lx(x)+ly(x)>=weight(x,y)\)。
相等子圖:設 G(V,E) 為二部圖, G'(V,E') 為二部圖的子圖。如果對於 G' 中的任何邊<x,y> 滿足, \(lx(x)+ ly(y)== weight(x,y)\),我們稱 G'(V,E') 為 G(V,E) 的等價子圖或相等子圖(是G的生成子圖)。
算法流程
這里有一篇比較好的男女找對象指南博客講解了KM算法的執行過程,生動而又形象。主要理解過程中是怎么滿足配對條件的,不滿足后怎么辦,過程中降低期望值d是怎么求得的,在配對失敗期望值進行更改以后發生了那些變化,這些變化為新的一輪的匹配帶來了什么?
算法原理:
搞清楚算法流程后對算法的原理就會很好奇(可能吧~),為什么要這樣做,這樣做后為什么會產生這些改變,為什么利用這些改變就可以得出最終結果?
在詳細介紹原理前,可以先看一下這篇博客(有圖),根據算法流程介紹原理,可以直觀感受一下,寫的還是很清晰的,就是字有點小
算法的正確性基於以下定理:(相關概念就可以倒回去看了)
若由二分圖中所有滿足\(lx(x)+ly(y)==weight(x,y)\)的邊(x,y)構成的子圖(稱做相等子圖)有完備匹配,那么這個完備匹配就是二分圖的最大權匹配。
因為對於二分圖的任意一個匹配,如果它包含於相等子圖,那么它的邊權和等於所有頂點的頂標和;如果它有的邊不包含於相等子圖,那么它的邊權和小於所有頂點的頂標和(即不是最優匹配)。所以相等子圖的完備匹配一定是二分圖的最大權匹配。
初始時為了使\(lx(x)+ly(y)>=weight(x,y)\)恆成立,令lx(x)為所有與頂點Xi關聯的邊的最大權,ly(y)=0。如果當前的相等子圖沒有完備匹配,就按下面的方法修改頂標以使擴大相等子圖,直到相等子圖具有完備匹配為止。
我們求當前相等子圖的完備匹配失敗了,是因為對於某個X頂點,我們找不到一條從它出發的交錯路。這時我們獲得了一棵交錯樹,它的葉子結點全部是X頂點(因為沒法跟Y匹配下去了,終止在X)。現在我們把交錯樹中X頂點的頂標全都減小某個值d,Y頂點的頂標全都增加同一個值d(注意是交錯樹中的頂點,也就是匹配的女生和被訪問的男生),那么我們會發現:(這個過程就對應於男女生配對中女生配對失敗后的情況)
1)兩端都在交錯樹中的邊(x,y),lx(x)+ly(y)的值沒有變化。也就是說,它原來屬於相等子圖,現在仍屬於相等子圖。
2)兩端都不在交錯樹中的邊(x,y),lx(x),ly(y)都沒有變化。也就是說,它原來屬於(或不屬於)相等子圖,現在仍屬於(或不屬於)相等子圖。
3)X端不在交錯樹中,Y端在交錯樹中的邊(x,y),它的lx(x)+ly(y)的值有所增大。它原來不屬於相等子圖,現在仍不屬於相等子圖。
4)X端在交錯樹中,Y端不在交錯樹中的邊(x,y),它的lx(x)+ly(y)的值有所減小。也就說,它原來不屬於相等子圖,現在可能進入了相等子圖,因而使相等子圖得到了擴大。
現在的問題就是求d值了。為了使\(lx(x)+ly(y)>=weight(x,y)\) 始終成立,且至少有一條邊進入相等子圖,d應該等於:{\(Minlx(x)+ly(y)-weight(x,y)|x在交錯樹中,y不在交錯樹中\)}。
以上是KM算法的基本思想。但是朴素的實現方法,時間復雜度為O(n4)——需要找\(O(n)\)次增廣路,每次增廣最多需要修改\(O(n)\)次頂標,每次修改頂標時由於要枚舉邊來求d值,復雜度為\(O(n^2)\)。實際上KM算法的復雜度是可以做到\(O(n^3)\)的。我們給每個Y頂點一個“松弛量”函數slack,每次開始找增廣路時初始化為無窮大。在尋找增廣路的過程中,檢查邊(x,y)時,如果它不在相等子圖中,則讓slack[y]變成原值與weight(x,y)-lx(x)-ly(y)的較小值(就是男生女生配對的求d的過程)。這樣,在修改頂標時,取所有不在交錯樹中的Y頂點的slack值中的最小值作為d值即可。但還要注意一點:修改頂標后,要把所有的不在交錯樹中的Y頂點的slack值都減去d(對應就是未被訪問的男生離女生更近了一步)。
代碼
以下代碼基於HDU2255奔小康賺大錢
#include <iostream>
#include <cstdio>
#include <memory.h>
#define max_n 305
#define INF 0x3f3f3f3f
using namespace std;
int n;//一個集合中的頂點數
int love[max_n][max_n];//兩個不同集合點之間的權值,即男女好感度
int ex_girl[max_n];//女生頂標
int ex_boy[max_n];//男生頂標
int vis_girl[max_n];//記錄女生是否參與匹配
int vis_boy[max_n];//記錄男生是否被嘗試匹配
int slack[max_n];//松弛函數,以求出頂標的最小改變量d
int match[max_n];//記錄匹配關系
int dfs(int girl)
{
vis_girl[girl] = 1;
for(int boy = 0;boy<n;boy++)
{
if(vis_boy[boy]) continue;//每次dfs每個男生都只能被訪問一次
int gap = ex_girl[girl]+ex_boy[boy]-love[girl][boy];//看看L(x)+L(y)-weight(x,y)的值
if(gap==0)//如果滿足匹配規則
{
vis_boy[boy] = 1;//當前男生已被訪問
if(match[boy]==-1||dfs(match[boy]))//如果男生無主或者他的女生可以拋棄他
{
match[boy] = girl;//就給男生匹配新的女生
return 1;//配對成功
}
}
else
{
slack[boy] = min(gap,slack[boy]);//如果無法滿足匹配規則,看男生要能跟女生匹配還差多少,不斷更新這個差值
}
}
return 0;
}
int KM()
{
memset(match,-1,sizeof(match));//全部標記為未匹配
memset(ex_boy,0,sizeof(ex_boy));//男生頂標初始化為零
for(int i = 0;i<n;i++)//求出女生頂標,即這個女生與男生好感度的最大值
{
ex_girl[i] = love[i][0];
for(int j = 1;j<n;j++)
{
ex_girl[i] = max(ex_girl[i],love[i][j]);
}
}
for(int i = 0;i<n;i++)//對每個女生開始匹配
{
fill(slack,slack+n,INF);//松弛函數初始化為無限大
while(1)
{
memset(vis_girl,0,sizeof(vis_girl));//每次dfs前清除標記數組
memset(vis_boy,0,sizeof(vis_boy));
if(dfs(i)) break;//當前女生匹配成功
int d = INF;//若未成功
for(int j = 0;j<n;j++)
{
if(vis_boy[j]==0)
{
d = min(d,slack[j]);//看最小需要將第多少期望值能夠匹配成功
}
}
for(int j = 0;j<n;j++)
{
if(vis_girl[j])
{
ex_girl[j] -= d;//對參與匹配的女生降低期望值
}
if(vis_boy[j])
{
ex_boy[j] += d;//對被訪問的男生提高期望值,上面兩個步驟使得
//1.參與匹配的女生跟滿足匹配規則匹配的男生仍然可匹配
//2.參與匹配的女生跟本未被訪問的男生可能滿足規則,擴大了匹配范圍
//3.未參與匹配的女生跟更不可能匹配被訪問的男生
//4.未參與匹配的女生跟未被訪問的男生依然不能匹配
}
else
{
slack[j] -= d;//未被訪問的男生離女生跟進了一步
}
}
}
}
int ans = 0;
for(int i = 0;i<n;i++)
{
if(match[i]>-1)
{
ans += love[match[i]][i];
}
}
return ans;
}
int main()
{
while(~scanf("%d",&n))
{
for(int i = 0; i<n; i++)
{
for(int j = 0; j<n; j++)
{
scanf("%d",&love[i][j]);
}
}
printf("%d\n",KM());
}
return 0;
}
參考文章
x_y_q_ ,帶權二分圖的最佳匹配(KM算法), https://blog.csdn.net/x_y_q_/article/details/51927054 (偏向原理)
SixDayCoder,二分圖的最佳完美匹配——KM算法,https://blog.csdn.net/sixdaycoder/article/details/47720471 (概念向)
wenr,KM算法詳解+模板,https://www.cnblogs.com/wenruo/p/5264235.html (算法流程介紹)
kuangbin, Kuhn-Munkres算法(二分圖最大權匹配),https://www.cnblogs.com/kuangbin/archive/2012/08/19/2646535.html (總結向)
