ps:推荐这篇博客:http://www.cnblogs.com/wenruo/p/5264235.html,很生动很形象,就是没有证明(你看了就知道多生动多形象了=_=)
再ps:先看二分图最大匹配可能会对理解该算法有所帮助
二分图最佳完美匹配也是二分图的经典问题之一,给出两个集合,X和Y,然后X中的元素和Y中的元素有边相连,一条边能匹配仅当两端的节点都没有匹配过,且每条边有权值,求权值最大的匹配。
在说到KM算法之前,先讲讲另类的解决方法。我们知道二分图最大匹配可以用最大流求解(二分图最大匹配是最大流的一种特例),所以二分图最佳完美匹配也可以用最小费用最大流求解。不过因为网络流效率不是太高,所以可能会被卡。
Kuhn-Munkres算法,简称KM算法,可以比较高效的解决二分图最佳完美匹配问题。我们给每个点都加一个顶标L,L(x)就表示x点的顶标,对于任意一条边,要满足L(x)+L(y)>=w(x,y)。(x∈X,y∈Y,下文均省略了)
设原图为G,当前子图M包含G的所有点,但只包含G中L(x)+L(y)=w(x,y)的边。可以证明,当当前子图M有完美匹配(X集合和Y集合中的所有点都被匹配过了)时,M就是权值最大的图。
简单证明(参考刘汝佳大神的蓝书):M有完美匹配,说明完美匹配的权值和等于所有顶标之和(全被选了),那么肯定是最优秀的匹配。
所以我们先从一个可行顶标开始处理(比如L(x)=max{L(y)|(x,y)∈E},L(y)=0),然后枚举每一个X集合中的点now,如果当前可行顶标不能满足now匹配(也就是不满足完美匹配),就要将顶标修改一下,使其满足now被匹配。
可是怎么修改呢?S[x]表示这次匹配x点是否被访问,T[y]表示这次匹配y点是否被访问,由于now不能匹配,所以我们需要将所有S[x]=true的点的顶标都减小一些,从而和T[y]=false的一些点可以连边,但是同时我们要保证顶标可行且原先匹配存在,所以要将T[y]=true的一些点的顶标都增加一些。增减多少呢?我们会发现增减MIN=min{L(x)+L(y)-w(x,y)|S[x]=true,T[y]=false}是最好的,因为这样既可以满足有新边加入,又可以满足顶标可行。
那么在匹配过后扫一趟就可以求出MIN。但是这样的复杂度是 O(n2) 的,我们可以给每一个T[y]=false的节点记一个松弛量(为什么叫松弛量,不知道QAQ)MINs[y]=min{L(x)+L(y)-w(x,y)|S[x]=true,(x,y)∈E},每次匹配的时候就可以顺便求出松弛量了,再扫一趟松弛量就可以 O(n) 得到MIN。
由于每次修改顶标后,至少一条边会可用(原来边也不会没用),所以总共修改顶标最多 O(m) 次。而每次验证匹配和修改顶标的复杂度是 O(n) 的。所以效率是 O(n∗m) (感觉可能比这个大,因为验证匹配由于要扫边所以比 O(n) 大,但每个点只会扫一次)。稀疏图效果较好。
我们会发现,如果不存在完美匹配,KM算法就无法进行(会因为找不到匹配的点卡住)。怎么办呢?其实这种情况存在“不选”这个操作,所以我们可以把不存在的边补上,边权给0,表示不选。
以HDU2255为例。
#include
#include
#include
using namespace std;
const int maxn=300,MAXINT=((1<<30)-1)*2+1;
int n,cst[maxn+5][maxn+5];
int Lx[maxn+5],Ly[maxn+5],MINs[maxn+5],who[maxn+5];
bool S[maxn+5],T[maxn+5];
bool Eoln(char ch) {return ch==10||ch==13||ch==EOF;}
char readc()
{
static char buf[100000],*l=buf,*r=buf;
if (l==r) r=(l=buf)+fread(buf,1,100000,stdin);
if (l==r) return EOF; else return *(l++);
}
int readi(int &x)
{
int tot=0,f=1;char ch=readc(),lst='+';
while ('9''0') {if (ch==EOF) return EOF;lst=ch;ch=readc();}
if (lst=='-') f=-f;
while ('0'<=ch&&ch<='9') tot=tot*10+ch-48,ch=readc();
x=tot*f;
return Eoln(ch);
}
bool Find(int x) //为x找匹配
{
S[x]=true;
for (int i=1;i<=n;i++) if (!T[i])
{
int s=Lx[x]+Ly[i]-cst[x][i];
if (!s) //这条边可以走
{
T[i]=true;
if (!who[i]||Find(who[i])) {who[i]=x;return true;}
} else
MINs[i]=min(MINs[i],s); //不可以走,修正松弛量
}
return false;
}
int KM()
{
memset(Ly,0,sizeof(Ly));
for (int i=1;i<=n;i++) Lx[i]=-MAXINT;
for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)
Lx[i]=max(Lx[i],cst[i][j]); //初始可行顶标
memset(who,0,sizeof(who));
for (int now=1;now<=n;now++) //为每一个x找匹配
{
for (int i=1;i<=n;i++) MINs[i]=MAXINT;
while (true) //直到匹配成功为止
{
memset(S,0,sizeof(S));memset(T,0,sizeof(T));
if (Find(now)) break; //匹配成功
int MIN=MAXINT;
for (int i=1;i<=n;i++) if (!T[i]) MIN=min(MIN,MINs[i]);
//刷出MIN
for (int i=1;i<=n;i++)
{
if (S[i]) Lx[i]-=MIN; //均减少MIN
if (T[i]) Ly[i]+=MIN; else //均加上MIN
MINs[i]-=MIN; //由于S[i]=true的Lx[i]都减少MIN了,所以松弛量减少MIN
}
}
}
int ans=0;for (int i=1;i<=n;i++) ans+=cst[who[i]][i]; //答案
return ans;
}
int main()
{
freopen("KM.in","r",stdin);
freopen("KM.out","w",stdout);
while (~readi(n))
{
for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)
readi(cst[i][j]);
printf("%d\n",KM());
}
return 0;
}