1、前言
學習是一個痛苦的過程,讓我們養(yǎng)成了不求甚解的習慣。
2、匈牙利算法
嗯,首先網上已經有很多啦。但是我覺得很多還是摳不清楚細節(jié),于是就有了這篇文章。 匹配就是一種兩兩不相接的邊的集合,二分匹配就是二分圖的匹配,匈牙利的基礎可以隨便看一篇。
bool find(int x){
int i,j;
for (j=1;j<=m;j++){ //掃描每個妹子
if (line[x][j]==true && used[j]==false)
//如果有曖昧并且還沒有標記過(這里標記的意思是這次查找曾試圖改變過該妹子的歸屬問題,但是沒有成功,所以就不用瞎費工夫了)
{
used[j]=1;
if (girl[j]==0 || find(girl[j])) {
//名花無主或者能騰出個位置來,這里使用遞歸
girl[j]=x;
return true;
}
}
}
return false;
}
int match ()
{
int all = 0;
for (i=1;i<=n;i++)
{
memset(used,0,sizeof(used)); //這個在每一步中清空
if find(i) all+=1;
}
return all;
}
以上代碼是我從一篇的鏈接中拿過來的,line數組存的是二分圖<X,Y>的邊權,不難證明從X中一個點x出發(fā)若匹配的數量增加,等價于存在一條從x出發(fā)的增廣路。算法的思想就是對X中每個點通過find來深搜增廣路。由于每次只會遞歸used為0的點。所以遞歸深度為O(n),find的復雜度是O(n2),整個算法的復雜度是O(n3)。
嗯,到這里沒什么問題,一般的blog討論到這里也結束了。但是當你仔細看find,會發(fā)現這個函數好像跟我們平時的搜索不太一樣。
bool find(int x){
int i,j;
for (j=1;j<=m;j++){
if (line[x][j]==true && used[j]==false)
{
used[j]=1;
if (girl[j]==0 || find(girl[j])) {
girl[j]=x;
return true;
}
//used[j]=0; 平時這里要將賦值過得used還原。
}
}
return false;
}
平時搜索失敗的時候是需要把上下文還原的,也就是去掉注釋。然而一旦去掉注釋,這將是一個指數復雜度的搜索算法。于是我們有理由相信,不需要還原used恰恰是匈牙利算法對搜索的一種優(yōu)化,而為什么可以這樣優(yōu)化,這種正確性則是我們需要證明的事,也是該算法的精髓。
我們考慮一個去掉上述注釋的find函數,此時就是普通的搜索,這樣算法肯定是對的,接下來我們要證明這樣搜索的結果與注釋時是一樣的。
我們要解決的問題是<X,Y>的二分圖中,已經used的部分組成的集合稱作前綴。初始時前綴為空集。從X中一個點x0出發(fā)在圖中找增廣路徑,查找所有邊,若對應的y之前未匹配則直接返回找到解。否則將y加入前綴,并從y之前匹配的點開始繼續(xù)搜索,搜索成功則直接返回答案,否則將該點剔除前綴。
不難發(fā)現以下結論。
- 在搜索過程中,除了找到增廣路結束搜索。否則每個匹配的Y中的點都對應同一個X中的點。
- 若從一個前綴為S時從x出發(fā)搜索失敗,則前綴為S超集時從x出發(fā)也搜索失敗。
以上發(fā)現比較顯然,就不證明啦。
定理1 若從一個前綴為S時搜索到y時繼續(xù)搜索失敗,則前綴為S超集時搜索到y繼續(xù)搜索也失敗。
證明:由搜索失敗知y肯定有匹配的點。由結論1知道在搜索成功前y匹配的點始終不會變化。再根據結論2可證明。
定義1 在前綴S時從x出發(fā)搜索和前綴T時從x出發(fā)搜索時都有解或者都無解,則稱S和T在x處前綴等價。
定理2 前綴S和前綴T在x處等價,前綴T和前綴P在x處等價,則前綴S和前綴P在x處等價。
證明:由 定義1知,顯然。
定理3 前綴S和前綴T在x處等價,若P是前綴S的超集,T是P的超集。則前綴S和前綴P在x處等價,前綴T和前綴P在x處等價。
證明:
前綴S與前綴T在x處等價,知前綴S和前綴T在x處搜索都有解或者都無解。
若前綴T和前綴S在x處有解,則由結論2的逆否命題及前綴T在x處有解知前綴P在x處有解。
若前綴T和前綴S在x處無解,則由結論2及前綴S在x處無解知前綴P在x處無解。
綜上所述,則前綴S和前綴P在x處等價,前綴T和前綴P在x處等價。
定理4 在前綴集合S時從y的匹配搜索時,設所有前綴S從y的匹配出發(fā)訪問過的搜索失敗的點為Q,則S和S并Q在y的匹配處前綴等價。
證明:由于還在搜索,所以目前的匹配都是失敗的。設從前綴S開始從y的匹配訪問過的點Q中,設至少一次在第k次遞歸中出現時的點的集合為C(k)。
Q = C(0) 并 C(1) 并 C(2) ...
下面用數學歸納法證明S和S并C(0)并C(1)...并C(n)在y處前綴等價。
對于C(0)中任意一個點y0,由于S并{y0}在y0處無解,則S并{y0}的超集在y0處也無解。所以y0出現在之后搜索的任何位置,從那里搜索必然無解。這相當于C(0)中任意一個點在之后被搜索到都將得不到解,則S并C(0)和S在y處前綴等價。
設n=k時,S和S并C(0)并C(1)...并C(k)在y處等價。
n=k+1是,設D=S并C(0)并C(1)...并C(k)。以前綴D從y的匹配開始搜索。由于C(k+1)中的點都可以從C(k)中走過來且都失敗了,因此若是從其他地方搜索到C(k+1)必然是D的超集由結論2也會失敗。故前綴D和D并C(k+1)在y匹配處前綴等價,又S和D在y匹配處前綴等價。由定理2知S和D并C(k+1)在y匹配處前綴等價,即S和S并C(0)并C(1)...并C(k)并C(k+1)在y匹配處前綴等價。
由于搜索是有限的,我們知道存在m使得k>m時C(k)為空集。
Q = C(0) 并 C(1) 并 C(2) ... 并C(m)。
S并C(0)并C(1)...并C(m)和S在y處等價,有S并Q和S在y處前綴等價。
定理5 在前綴集合S時從y的匹配搜索時,設所有由深搜訪問過的搜索失敗的點的集合為Q,則S和S并Q在y的匹配處前綴等價。
證明:顯然,任何一個訪問過的點,一定是由到y的過程中某個階段失敗的。由定理4,可以把當時的前綴替換為前綴并上該前綴起搜索失敗點的集合。這些所有階段的并集是Q。則到y時前綴可以替換為S并Q。得證。
由定理5知可以把之前訪問過的點并入前綴,即used。所以加不加注釋,搜索出來的結果是一樣的。
3、KM算法
KM算法的話,就是二分圖的匹配帶權,特別注意不是求數量最多的匹配,而是求匹配中權和最大的匹配。網上很多O(n^4)實現。這里有一個比較好的O(n^3)實現。
KM算法的圖使用鄰接矩陣來表示,沒有邊的點之間加一個權值為0的邊。這樣可以保證無論怎么連都存在完美匹配。然后通過給二分圖的兩邊的點頂標,維護每條邊的權值都小于兩邊的頂標和,對于一個完備匹配,若每條邊的權值等于兩個點的頂標和,則一定是最大權匹配。因為該權和等于所有頂標和,若存在更大的權匹配,則與每條邊的權值都小于兩邊的頂標和矛盾。算法的具體細節(jié)在鏈接里已經講得比較清楚啦。
下面講一些KM的討論,若邊有負權,算法仍然成立。情況與加好零邊后給所有的邊加上最小負數的絕對值再跑算法一直。因為一定有完美匹配,多出來的值是固定的。
若二分圖兩邊點的個數不相等,這樣匹配就不能覆蓋所有頂標,二分圖的正確性不能保證,因此KM算法只能解決二分圖兩邊點數相等的情況。