写在之前:更多二分图知识,请关注--->二分图知识导航篇
引述
KM算法全称Kuhn-Munkres,是一种求二分图完全、完美、最佳等匹配的方法,其实三个形容词都是指两个集内的所有顶点能够一一匹配,并且所获得的权值最大或最小。如果了解过,其实用最小费用最大流也可以,不过本文就讲解KM算法。在学习的时候,发现KM里面有几处思想挺难懂的,因为没见过吧,所以我将着重讲解下里面独特的思想。
算法步骤
之前求不带权值的二分图匹配时,我们用的方法是匈牙利算法。而这次换成带权图,并且要求出最佳匹配,也离不开匈牙利算法,可以说匈牙利算法的思想也含在其内。
第一步:初始化可行标杆(顶标)的值;
第二步:用匈牙利算法寻找完全匹配;
第三步:若未找到完全匹配,则修改可行标杆(顶标)的值;
第四步:重复第二、三步,直到找到相等子图的完全匹配为止。
我用橘色标注了比较难理解的点,大概是以下几个内容:1、初始化标杆;2、寻找完全匹配;3、修改标杆;4、重复2,3步
重点理解
一、初始化标杆
(1)首先什么是标杆?标杆的作用?
标杆是指在出发集X和被匹配集Y中,所有顶点都有一个特殊的值,取自权值,但又不是权值。其实标杆的意思就是一个标准,大家朝着这个标准去做,所以标杆就是给每个顶点设立一个标准。
而这标准的作用是用于判断所匹配的顶点,连成边后,是不是集合中权值较大的边。
因为是顶点的标杆,所以也可以称为顶标,个人习惯而已。
(2)如何初始化?
这一点是要看,你找的是最大权值还是最小权值。如果是最大权值的完全匹配,那就一开始将出发集X内所有顶点的标杆,设置为它所能连边的最大权值,将配匹配集Y内所有顶点预设为0。这一点不难理解,
我们来做个假设:假设你要选出10个数字,数字的值从1到10,并且这选出的10个数字加起来要尽量大。你怎么选?那你肯定是选10个10,加起来100嘛,不可能选10个11或者更高的数字,因为规定了最大只能选10嘛。那条件改变一下,如果你手上可选的数字有数量的限制,那么你就只能从大到小选择,这也就是贪心的思想。
那我们回到标杆初始化的理念,因为我们要尽量连接权值最大的边,但每条边不一定能很好的分配,因为A连接B是最大的权值,结果C连接B也是最大的结果,这就冲突了,那我们就会再找另外一条比较大且没被选的边。所以如果要找最大权值的完全匹配,那就要先预设一个最大的情况,也就是再怎么大也不会大过这个预设情况。
还有一点就是,为什么要将匹配集Y内顶点预设为0呢?这是一个独特的想法:标杆值要满足Cx+Cy=w[x][y];这里的C代表标杆,w表示边的权值,所以公式的意思是:X集标杆A+Y集标杆B,要等于边ab。所以我们一开始将所有Cx=max{w[x][y]},Cy就肯定为0啦,如图1所示。
二、寻找完全匹配
(1)退出条件
完全匹配,所以一定要为所有顶点配对成功,才能退出。也就是遍历所有出发集X的顶点,然后进入whlie循环,直到找到了最优匹配值才退出,然后匹配下一个顶点。
(2)如何匹配呢?(颜色1表示出发集X内的点,颜色2表示被匹配集Y内的点)
首先和匈牙利算法的基本理念一样,在这重复描述一下:首先读入出发集X里某顶点A,然后进入循环(遍历被匹配集Y里的顶点),如果找到了一个顶点B满足:1、可以与点A相连;2、并且在这场循环里,还没被配对或重新配对过;如果都满足就进入下层判断:1、如果此点B从来还没被匹配过(即还没加入过这个匹配圈);2、如果此点B被匹配了,但是跟它匹配的点还可以找到其他的点。如果这两个条件符合一个,就可以将点B就会被匹配给点A。
那KM算法不同地方在哪呢?让我再描述下KM匹配过程(红色标注不同):首先读入出发集X里某顶点A,给点A做个记号(在修改标杆时会用到) ,然后进入循环(遍历被匹配集Y里的顶点,这里Y集内的点数跟X集一样,因为要一一匹配),如果找到了一个顶点B满足:1、是一条与点A相连的较优边(这里所有点都可以与点A相连,但要找一条较优的边;较优可以是较大或较小,根据具体情况来看,而且最优规则是:Cx[u]+Cy[v] == weight[u][v],这里就是标杆值体现的作用了);2、并且在这场循环里,还没被配对或重新配对过;如果都满足就进入下层判断:1、如果此点B从来还没被匹配过(即还没加入过这个匹配圈);2、如果此点B被匹配了,但是跟它匹配的点还可以找到其他的点。如果这两个条件符合一个,就可以将点B就会被匹配给点A。
从上面可以看出,最大的不同就是要连一条较优的边(标杆的作用),而不仅仅是可以连的边。
三、修改标杆
在进行了一次匹配寻找后,发现没有适合的点,那就只能看看还有没有其他点,连接后是一条权值比较优的边。
(1)修改范围
因为进行了匹配寻找,所有被匹配过的边都有记录,也就是X集,Y集内匹配过的点有记号。那我们接下来要找的点就是X集里已经匹配过的点,和Y集内还没有用过的点,因为要看看有没有新的比较优的边。不看X集未匹配点,是因为还没轮到他匹配,所以在当前匹配圈内寻找最优。
(2)修改准则
要修改,就要符合一定准则。在这里准则就是Cx[u]+Cy[v] == weight[u][v],怎么理解呢?记得我们初始化的步骤吗,一开始将X集内所有点的标杆都标注成,与自身所连最大的权值。但这种连法会产生冲突,所以我们会再选择连接其他边,权值减少是一定的。 下面我们再做个场景:假如出发集X有点A,B;被匹配集Y有点B,E。AB边权值是10,CB边权值是11,而AD边权值是9,CE边是8。
1、首先预设标杆Ca为10,标杆Cc为11,标杆Cb和Ce都是0,
2、然后开始连边:AB已经连好了,现在到C去连,结果发现最好的点B被A连了,那就要重新换边,那到底谁换呢?是C连E的权值8,还是A重新练D权值9呢?权衡一下AB+CE的权值是18,AD+CB权值是20,所以我们选择让A重新连上D。
3、下面换用标杆来表示过程2:首先AB连好后,标杆没变化;当C选择是,不能找B也就是匹配不成功(因为现在只有B加入了匹配全),所以要进行标杆修改:(1)Cc+Ce-w[c][e] = 11 + 0 - 8 = 3; (2)Ca+Cd - w[a][d] = 10 + 0 - 9 = 1; 于是发现(2)比(1)要小,也就是减少的比较少,所以我们选择让A重新与D连边,点C连B。
所以就只要看哪种修改标杆方式,使得权值减少更少,就选哪种。于是有了一个变量d = min { Cx[i] + Cy [j] - weight[i][j] },取最少的减少量。
(3)修改方式
当我们取到了最少的减少量d后,还需要用到标杆。这时我们将出发集X里已经匹配过的点的标杆值统一减去d,然后将被匹配集Y里所有已经匹配过的点的标杆值加上d。这样做,一是记录了标杆值减少后的状态,二是又保证了Cx[u]+Cy[v] == weight[u][v]的平衡。
四、重复步骤2、3
如果弄懂了前面三步,这一步就没问题了,因为我已经在第二步就有所解释了。再描述一遍: 完全匹配,所以一定要为所有顶点配对成功,才能退出。也就是遍历所有出发集X的顶点,然后进入whlie循环,直到找到了最优匹配值才退出,然后匹配下一个顶点。
第二步:用匈牙利算法寻找完全匹配;
第三步:若未找到完全匹配,则修改可行标杆(顶标)的值;
未集合X里的顶点寻找匹配就是步骤2,如果没有找到就进行第3步,一直重复这两个步骤,直到在Y集里找到了合适的顶点,就退出,再为下一个集合X内顶点匹配。
代码实现
最后说下最小匹配和最大匹配,在这里的区别:其实只要初始化的时候,将权值相反就好了,匹配过程不用修改。
下面代码来自这个博文
#include <iostream>
#include <cstdio>
#include <memory.h>
#include <algorithm>
using namespace std;
#define MAX 100
int n;
int weight[MAX][MAX]; //权重
int Cx[MAX],Cy[MAX]; //定点标号
bool sx[MAX],sy[MAX]; //记录寻找增广路时点集x,y里的点是否搜索过
int match[MAX]; //match[i]记录y[i]与x[match[i]]相对应
bool search_path(int u) { //给x[u]找匹配,这个过程和匈牙利匹配是一样的
sx[u]=true;
for(int v=0; v<n; v++){
if(!sy[v] && Cx[u]+Cy[v] == weight[u][v]){
sy[v]=true;
if(match[v]==-1 || search_path(match[v])){ //如果第v个y点还没被占,或者第v个y点还可以找到其他可搭配的x点
match[v]=u;
return true;
}
}
}
return false;
}
int Kuhn_Munkras(bool max_weight){
if(!max_weight){ //如果求最小匹配,则要将边权取反
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
weight[i][j]=-weight[i][j];
}
//初始化顶标,Cx[i]设置为max(weight[i][j] | j=0,..,n-1 ), Cy[i]=0;
for(int i=0;i<n;i++){
Cy[i]=0;
Cx[i]=-0x7fffffff;
for(int j=0;j<n;j++)
if(Cx[i]<weight[i][j])
Cx[i]=weight[i][j];
}
memset(match,-1,sizeof(match));
//不断修改顶标,直到找到完备匹配或完美匹配
for(int u=0;u<n;u++){ //为x里的每一个点找匹配
while(1){
memset(sx,0,sizeof(sx));
memset(sy,0,sizeof(sy));
if(search_path(u)) //x[u]在相等子图找到了匹配,继续为下一个点找匹配
break;
//如果在相等子图里没有找到匹配,就修改顶标,直到找到匹配为止
//首先找到修改顶标时的增量inc, min(Cx[i] + Cy [i] - weight[i][j],inc);,Cx[i]为搜索过的点,Cy[i]是未搜索过的点,因为现在是要给u找匹配,所以只需要修改找的过程中搜索过的点,增加有可能对u有帮助的边
int inc=0x7fffffff;
for(int i=0;i<n;i++)
if(sx[i])
for(int j=0;j<n;j++)
if(!sy[j]&&((Cx[i] + Cy [j] - weight[i][j])<inc))
inc = Cx[i] + Cy [j] - weight[i][j] ;
//找到增量后修改顶标,因为sx[i]与sy[j]都为真,则必然符合Cx[i] + Cy [j] =weight[i][j],然后将Cx[i]减inc,Cy[j]加inc不会改变等式,但是原来Cx[i] + Cy [j] !=weight[i][j]即sx[i]与sy[j]最多一个为真,Cx[i] + Cy [j] 就会发生改变,从而符合等式,边也就加入到相等子图中
for(int i=0;i<n;i++){
if(sx[i]) //如果点x在S集合里
Cx[i]-=inc;
if(sy[i]) //如果点y在T集合里
Cy[i]+=inc;
}
}
}
int sum=0;
for(int i=0;i<n;i++)
if(match[i]>=0){
sum+=weight[match[i]][i];
// cout << match[i] <<" : "<< weight[match[i]][i] << endl;
}
if(!max_weight)
sum=-sum;
return sum;
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
scanf("%d",&weight[i][j]);
printf("%d\n",Kuhn_Munkras(1));
return 0;
}
可以用这道例题HDU-2255例题来练练手,相关博文还有这个和这个。