匈牙利演算法和KM演算法的理解
匈牙利演算法
二分圖的最大匹配可以轉換為一個網路流的問題,但是我們一般使用匈牙利演算法,這種演算法更易於理解,方便編寫。
介紹這個演算法之前,首先要介紹一些必要的概念。
交錯路 : 從一個未匹配點出發,依次遍歷未匹配邊、匹配邊、未匹配邊,這樣交替下去,這條路徑稱為交錯路。
增廣路 : 從一個未匹配點出發,依次遍歷未匹配邊、匹配邊、未匹配邊,這樣交替下去,如果最後一個點是未匹配點,這條路徑稱為增廣路。換句話說,起點和終點都為未匹配點的交錯路為增廣路(特別提醒,這裡的增廣路和網路流中的增廣路的意義不同)
如圖所示,圖6 是圖5 的其中一條增廣路,可以看出由未匹配點9出發,依次沿著邊
e
d
g
e
(
9
,
4
)
−
>
e
d
g
e
(
4
,
8
)
−
>
e
d
g
e
(
8
,
1
)
−
>
e
d
g
e
(
1
,
6
)
−
>
e
d
g
e
(
6
,
2
)
e d g e ( 9 , 4 ) −> e d g e ( 4 , 8 ) −> e d g e ( 8 , 1 ) −> e d g e ( 1 , 6 ) −> e d g e ( 6 , 2 )
觀察圖6我們可發現增廣路的一些特點。
-
增廣路一定有奇數條邊。
-
增廣路中未匹配邊一定比匹配邊多一條(因為是從未匹配點出發走交錯路到未匹配點結束)
這裡其實就表明了研究增廣路的意義。
如果找到了一條增廣路,那麼將未匹配點與匹配邊的身份調換,那麼匹配的邊數就多了一條,這樣直到找不到增廣路為止,那麼整個圖的匹配的邊數一定最大,也就是找到了二分圖的最大匹配。
這裡的身份調換是指 :
原來匹配的邊為edge(1,6), edge (4,8),匹配邊數為2 。找到一條增廣路(這裡不一定從9開始找,任何一個未匹配點都可以)後,現在匹配的邊為edge(2,6), edge(1,8), edge(4,9), 匹配邊數為3。
匈牙利演算法正是利用了增廣路的這個性質,從X集合中找到一個未匹配點,尋找增廣路,找到了匹配數+1,如果沒有找到,那麼從X中找到下一個未匹配的點,再次尋找增廣路…重複上述過程,直到X集合中的所有節點都被“增廣”完畢,無論如何都找不到增廣路,那麼整個圖的匹配數就最大了
虛擬碼:
void hungary()//匈牙利演算法
{
for i->1 to nx//對於集合X中的每一個節點
if (從未匹配點i出發有增廣路)
匹配數++;
輸出 匹配數;
}
bool findpath(x)//尋找從x出發的對應項出的可增廣路
{
//crossPath[x] = true;這條語句可能會有很多人加上,但是實際上crossPtah總是記錄集合Y中的節點是否在交錯路上
for each edge(x,y) in G.E
{
if(y is not in crossPath)
{
add y into crossPath
lastX = match[y];//lastX是X集合的上一個與y匹配的節點
if(y is not matched or findpath(lastX))//如果y已經被了,那麼試試從lastX能不能另外找到一條增廣路,把當前增廣路讓給現在的x
{
match[y] = x;
//match[x] = y,wrong !
return true;//從x出發有增廣路
}
}
}
return false;//從x出發沒有增廣路
}
KM演算法
如果二分圖的每條邊都有一個權重,要求一種完備匹配方案,使得所有匹配邊的權重和最大,記作最佳完美匹配。一般使用KM演算法解決該問題
KM演算法,是對匈牙利演算法的一種貪心擴充套件。
流程
Kuhn-Munkras演算法(即KM演算法)流程:
- 初始化可行頂標的值 (設定lx,ly的初始值)
- 用匈牙利演算法尋找相等子圖的完備匹配
- 若未找到增廣路則修改可行頂標的值
- 重複(2)(3)直到找到相等子圖的完備匹配為止
KM演算法的核心部分即控制修改可行頂標的策略使得最終可到達一個完美匹配。
虛擬碼:
bool findpath(x)
{
visx[x] = true;
for(int y = 1 ; y <= ny ; ++y)
{
if(!visy[y] && lx[x] + ly[y] == weight(x,y)) //y不在交錯路中且edge(x,y)必須在相等子圖中
{
visy[y] = true;
if(match[y] == -1 || findpath(match[y]))//如果y還為匹配或者從y的match還能另外找到一條匹配邊
{
match[y] = x;
return true;
}
}
}
return false;
}
void KM()
{
for(int x = 1 ; x <= nx ; ++x)
{
while(true)
{
memset(visx,false,sizeof(visx));//訪問過X中的標記
memset(visy,false,sizeof(visy));//訪問過Y中的標記
if(findpath(x))//找到了增廣路,跳出繼續尋找下一個
break;
else
{
for(int i = 1 ; i <= nx ; ++i)
{
if(visx[i])//i在交錯路中
{
for(int j = 1 ; j <= ny ; ++j)
{
if(visy[j])//j不在交錯路中,對應第二類邊
delta = Min(delta,lx[x] + ly[y] - weight(i,j))
}
}
}
for(int i = 1 ; i <= nx ; ++i)//增廣路中xi - delta
if(visx[i])
lx[i] -= delta;
for(int j = 1 ; j <= ny ; ++j)//增廣路中yj + delta
if(visy[j])
ly[j] += delta;
}
}
}
參考連結:
https://blog.csdn.net/sixdaycoder/article/details/47680831
https://blog.csdn.net/sixdaycoder/article/details/47720471