帶權二分匹配——KM演算法
上一篇中,我們講到如果不帶有權值,單純尋找最大匹配數的話,匈牙利演算法完全可以滿足,可是,許多問題中,通過帶權值我們可以找到最大或者最小化策略,這樣的問題,我們就要用到KM演算法。
先貼一道模板題:
奔小康賺大錢
傳說在遙遠的地方有一個非常富裕的村落,有一天,村長決定進行制度改革:重新分配房子。
這可是一件大事,關係到人民的住房問題啊。村裡共有n間房間,剛好有n家老百姓,考慮到每家都要有房住(如果有老百姓沒房子住的話,容易引起不安定因素),每家必須分配到一間房子且只能得到一間房子。
另一方面,村長和另外的村領導希望得到最大的效益,這樣村裡的機構才會有錢.由於老百姓都比較富裕,他們都能對每一間房子在他們的經濟範圍內出一定的價格,比如有3間房子,一家老百姓可以對第一間出10萬,對第2間出2萬,對第3間出20萬.(當然是在他們的經濟範圍內).現在這個問題就是村領導怎樣分配房子才能使收入最大.(村民即使有錢購買一間房子但不一定能買到,要看村領導分配的).
Input
輸入資料包含多組測試用例,每組資料的第一行輸入n,表示房子的數量(也是老百姓家的數量),接下來有n行,每行n個數表示第i個村名對第j間房出的價格(n<=300)。
Output
請對每組資料輸出最大的收入值,每組的輸出佔一行。
Sample Input
2
100 10
15 23
Sample Output
123
中文題,題意很明白,如果從網上直接搜,一堆增廣路等若干概念總是把人弄得雲裡霧裡的,G_lory模仿上一篇博文中提到的趣寫演算法寫出了KM演算法的通俗講法。
看完以後是不是恍然大悟?如果還不明白也不要緊,我將結合我寫的程式碼和上面那篇文章的思路給大家一份比較詳細的註釋,幫助大家進一步理解。
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
#include <cstring>
#define maxsize 310
#define inf 0x3fffff
#define min(x,y) x>y?y:x;
#define max(x,y) x>y?x:y;
//儲存權值
int map[maxsize][maxsize];
//lx相當於女生好感度,ly相當於男生好感度
int lx[maxsize],ly[maxsize];
//女、男生是否訪問的標記
int visx[maxsize],visy[maxsize];
//以右側男生為參照,需要降低的最少好感度
int slack[maxsize];
//記錄男女生匹配關係
int link[maxsize];
//總數量,村子與村民數量一樣
int num;
//為第x個房子找歸宿
int dfs(int x)
{
//這一輪該點訪問過
visx[x]=1;
int i;
for(i=1;i<=num;i++)
{
//每一輪男生只訪問一次
if(visy[i]==1) continue;
//如果找到滿足的邊
if(visy[i]==0&&map[x][i]-lx[x]-ly[i]==0)
{
//男生訪問過標記
visy[i]=1;
//類似匈牙利演算法,為前面的匹配重新安排
if(link[i]==-1||dfs(link[i]))
{
link[i]=x;
return 1;
}
}
//關鍵來了,這一步將N四次方的複雜度將為N三次方
//對於不能滿足好感度相加等於權值的點,應該在最後根據是否重新匹配成功調整期望值
//如果從頭開始計算,那麼要把n各點都遍歷,複雜度上升
//利用這一步,在每次找到不滿足邊的同時計算需要最小降低的期望值,並進行更新,節省了時間
else
{
slack[i]=min(slack[i],fabs(map[x][i]-lx[x]-ly[i]));
}
}
return 0;
}
int km()
{
int i,j;
//男生好感度初始為0
memset(ly,0,sizeof(ly));
//匹配關係初始化
memset(link,-1,sizeof(link));
//因為是找女生相連邊的最大值,初始化為一個極小的數
for(i=1;i<=num;i++)
{
lx[i]=-inf;
}
for(i=1;i<=num;i++)
{
for(j=1;j<=num;j++)
{
//為女生初始化好感度
lx[i]=max(lx[i],map[i][j]);
}
}
//為房子找主人,權值就是價格,因為出價按照房子的順序,所以房子在左列,相當於上文的女生
for(i=1;i<=num;i++)
{
//每次匹配,都要先把最小降低值初始化為一個極大的數,方便不斷縮小,我就因為忘了每次初始化,改了好久
for(j=1;j<=num;j++)
slack[j]=inf;
//直到當前找到一個可行方案才會尋找下一個房子的歸宿
//另外,由於左邊期望值降低,右邊升高,所以後續找匹配找到的機會會上升,因為條件更容易達到
while(1)
{
//訪問歸零
memset(visx,0,sizeof(visx));
memset(visy,0,sizeof(visy));
//如果找到可行方案,跳出迴圈,繼續下一個房子
if(dfs(i)) break;
//否則
int m=inf;
//先找到所有降低值中的最小值
for(j=1;j<=num;j++)
{
if(!visy[j])
m=min(m,slack[j]);
}
//按照上文,左側訪問點降低,右側訪問點升高,右側未訪問點期望降低值減小
for(j=1;j<=num;j++)
{
if(visx[j]) lx[j]-=m;
if(visy[j]) ly[j]+=m;
else slack[j]-=m;
}
}
}
int res=0;
//只累加所有匹配邊的權值
for(i=1;i<=num;i++)
{
if(link[i]!=-1)
res+=map[link[i]][i];
}
return res;
}
int main()
{
while(cin>>num)
{
memset(map,0,sizeof(map));
int i,j;
for(i=1;i<=num;i++)
{
for(j=1;j<=num;j++)
{
scanf("%d",&map[i][j]);
}
}
printf("%d\n",km());
}
return 0;
}
本人也是演算法萌新,如果有問題,歡迎大家批評指正,或者和我討論。