二分图的完全匹配---KM算法

  写在之前:更多二分图知识,请关注--->二分图知识导航篇

  引述

   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
图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例题来练练手,相关博文还有这个这个

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值