KM算法详解

参考:

  1. https://www.cnblogs.com/logosG/p/logos.html
  2. http://www.mamicode.com/info-detail-2527621.html
  3. https://blog.csdn.net/songbai1997/article/details/82014828
  • KM算法(Kuhn-Munkres),用来求带权二分图的最大权匹配(最优匹配)

一、KM算法详解

1. 问题思考

  • 示例:如果一个公司里每个员工做每件工作的效率各不相同,我们如何得到一个最优匹配使得整个公司的工作效率最大呢?
    在这里插入图片描述
    这种问题被称为带权二分图的最优匹配问题,可由KM算法解决

    比如上图,A做工作a的效率为3,做工作c的效率为4…以此类推。

    不了解KM算法的人如何解决这个问题?我们只需要用匈牙利算法找到所有的最大匹配,比较每个最大匹配的权重,再选出最大权重的最优匹配即可。这不失为一个解决方案,但是,如果公司员工的数量越来越多,此种算法的实行难度也就越来越大,我们必须另辟蹊径KM算法

2. 算法步骤

KM算法解决此题的步骤如下所示:

  1. 首先对每个顶点赋值,将左边的顶点赋值为最大权重,右边的顶点赋值为0。

    如图,我们将顶点A赋值为其两边中较大的4。
    在这里插入图片描述

  2. 进行匹配,我们匹配的原则是:只与权重相同的边匹配,若是找不到边匹配,对此条路径的所有左边顶点-1,右边顶点+1,再进行匹配,若还是匹配不到,重复+1和-1操作。(这里看不懂可以跳过,直接看下面的操作,之后再回头来看这里。)

    对A进行匹配,符合匹配条件的边只有Ac边。

    匹配成功!
    在这里插入图片描述

  3. 接下来我们对B进行匹配,顶点B值为3,Bc边权重为3,匹配成~ 等等,A已经匹配c了,发生了冲突,怎么办?我们这时候第一时间应该想到的是,让B换个工作,但根据匹配原则,只有Bc边 3+0=3 满足要求,于是B不能换边了,那A能不能换边呢?对A来说,也是只有Ac边满足4+0=4的要求,于是A也不能换边,走投无路了,怎么办?
    在这里插入图片描述

  4. 从常识的角度思考:其实我们寻找最优匹配的过程,也就是帮每个员工找到他们工作效率最高的工作,但是,有些工作会冲突,比如现在,B员工和A员工工作c的效率都是最高,这时我们应该让A或者B换一份工作,但是这时候换工作的话我们只能换到降低总体效率值的工作,也就是说,如果令R=左边顶点所有值相加,若发生了冲突,则最终工作效率一定小于R,但是,我们现在只要求最优匹配,所以,如果A换一份工作降低的工作效率比较少的话,我们是能接受的(对B同样如此)

    在KM算法中如何体现呢?

    现在参与到这个冲突的顶点是A,B和c,令所有左边顶点值-1,右边顶点值+1,即 A-1,B-1,c+1,结果如下图所示。
    在这里插入图片描述

  5. 我们进行了上述操作后会发现,若是左边有n个顶点参与运算则右边就有n-1个顶点参与运算,整体效率值下降了1*(n-(n-1))=1 ,而对于A来说,Ac本来为可匹配的边,现在仍为可匹配边(3+1=4),对于B来说,Bc本来为可匹配的边,现在仍为可匹配的边(2+1=3),我们通过上述操作,为A增加了一条可匹配的边Aa,为B增加了一条可匹配的边Ba。

    现在我们再来匹配,对B来说,Ba边 2+0=2,满足条件,所以B换边,a现在为未匹配状态,Ba匹配!
    在这里插入图片描述

  6. 我们现在匹配最后一条边C,Cc:5+1!=5,C边无边能匹配,所以C-1。
    在这里插入图片描述

  7. 现在Cc边 4+1=5,可以匹配,但是c已匹配了,发生冲突,C此时不能换边,于是便去找A,对于A来说,Aa此时也为可匹配边,但是a已匹配,A又去找B。
    在这里插入图片描述

  8. B现在无边可以匹配了,2+0!=1 ,现在的路径是C→c→A→a→B,所以A-1,B-1,C-1,a+1,c+1。如下图所示。
    在这里插入图片描述

  9. 对于B来说,现在Bb:1+0=1 可匹配!
    在这里插入图片描述

  10. 使用匈牙利算法,对此条路径上的边取反(匈牙利路径是Cc-cA-Aa-aB-Bb,其中aB和cA是已经匹配的,取反就剩下了,Cc/Aa/Bb 三组匹配,至此,两组匹配变成三组匹配,算法完成)。
    在这里插入图片描述
    如图,便完成了此题的最优匹配。

    读者可以发现,这题中冲突一共发生了3次,所以我们一共降低了3次效率值,但是我们每次降低的效率值都是最少的,所以我们完成的仍然是最优匹配!

  • 这就是KM算法的整个过程整体思路就是:每次都帮一个顶点匹配最大权重边,利用匈牙利算法完成最大匹配,最终我们完成的就是最优匹配

二、KM算法实现:

#include <iostream>
#include <cstring>
#include <cstdio>
#include <vector>
#include <map>
using namespace std;
typedef long long ll;
const int maxn = 300 + 10;
const int INF = 0x3f3f3f3f;

int wx[maxn], wy[maxn];//每个点的顶标值(需要根据二分图处理出来)
int cx[maxn], cy[maxn];//每个点所匹配的点
int visx[maxn], visy[maxn];//每个点是否加入增广路
int cntx, cnty;//分别是X和Y的点数
int Map[maxn][maxn];//二分图边的权值
int slack[maxn];//边权和顶标最小的差值

bool dfs(int u)//进入DFS的都是X部的点
{
    visx[u] = 1;//标记进入增广路
    for(int v = 1; v <= cnty; v++)
    {
        if(!visy[v] && Map[u][v] != INF)//如果Y部的点还没进入增广路,并且存在路径
        {
            int t = wx[u] + wy[v] - Map[u][v];
            if(t == 0)//t为0说明是相等子图
            {
                visy[v] = 1;//加入增广路

                //如果Y部的点还未进行匹配
                //或者已经进行了匹配,可以从原来的匹配反向找到增广路
                //那就可以进行匹配
                if(cy[v] == -1 || dfs(cy[v]))
                {
                    cx[u] = v;
                    cy[v] = u;//进行匹配
                    return 1;
                }
            }
            else if(t > 0)//此处t一定是大于0,因为顶标之和一定>=边权
            {
                slack[v] = min(slack[v], t);
                //slack[v]存的是Y部的点需要变成相等子图顶标值最小增加多少
            }
        }
    }
    return false;
}

int KM()
{
    memset(cx, -1, sizeof(cx));
    memset(cy, -1, sizeof(cy));
    memset(wx, 0, sizeof(wx));//wx的顶标为该点连接的边的最大权值
    memset(wy, 0, sizeof(wy));//wy的顶标为0
    for(int i = 1; i <= cntx; i++)//预处理出顶标值
    {
        for(int j = 1; j <= cnty; j++)
        {
            if(Map[i][j] == INF)continue;
            wx[i] = max(wx[i], Map[i][j]);
        }
    }
    for(int i = 1; i <= cntx; i++)//枚举X部的点
    {
        memset(slack, INF, sizeof(slack));
        while(1)
        {

            memset(visx, 0, sizeof(visx));
            memset(visy, 0, sizeof(visy));
            if(dfs(i))break;//已经匹配正确


            int minz = INF;
            for(int j = 1; j <= cnty; j++)
                if(!visy[j] && minz > slack[j])
                    //找出还没经过的点中,需要变成相等子图的最小额外增加的顶标值
                    minz = slack[j];
            //和全局变量不同的是,全局变量在每次while循环中都需要赋值成INF,每次求出的是所有点的最小值
            //而slack数组在每个while外面就初始化好,每次while循环slack数组的每个值都在用到
            //在一次增广路中求出的slack值会更准确,循环次数比全局变量更少


            //还未匹配,将X部的顶标减去minz,Y部的顶标加上minz
            for(int j = 1; j <= cntx; j++)
                if(visx[j])wx[j] -= minz;
            for(int j = 1; j <= cnty; j++)
                //修改顶标后,要把所有不在交错树中的Y顶点的slack值都减去minz
                if(visy[j])wy[j] += minz;
                else slack[j] -= minz;
        }
    }

    int ans = 0;//二分图最优匹配权值
    for(int i = 1; i <= cntx; i++)
        if(cx[i] != -1)ans += Map[i][cx[i]];
    return ans;
}
int n, k;
int main()
{
    while(scanf("%d", &n) != EOF)
    {
        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= n; j++)
                scanf("%d", &Map[i][j]);
        }
        cntx = cnty = n;
        printf("%d\n", KM());
    }
    return 0;
}
  • 32
    点赞
  • 92
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值