二分图最大权匹配——KM算法

前言

  • 这东西虽然我早就学过了,但是最近才发现我以前学的是假的,心中感慨万千(雾),故作此篇。

    简介

  • 带权二分图:每条边都有权值的二分图
  • 最大权匹配:使所选边权和最大的匹配
  • KM算法,全称Kuhn-Munkres算法,是用于解决最大权匹配的一种算法。
  • 根据我的理解,该算法算是一种基于贪心的松弛算法,它通过设置顶标将原问题转化为求一个完备匹配(完备匹配:匹配数=min(左部点数,右部点数))。

    流程

  • 设左部中点\(x\)的顶标\(wx_x\)、右部中点\(y\)的顶标\(wy_y\)。初始时\(wx_u=\max\{w_{u,v}\}\)\(wy_v=0\)
  • 我们扫一遍左部,每扫到一个\(x\)点,尝试增广,我们只能走满足条件\(wx_u+wy_v=w_{u,v}\)的边;这种边构成了原图的相等子图(不要问我为什么,它就叫这个名字)。我们增广失败,将访问过的点(包括增广失败的点)形成的树称为交错树,该树显然所有叶子都是\(x\)点。
  • 接下来即是算法关键:我们为扩大相等子图(使当前的\(x\)尽量匹配上),修改所有交错树中的点的顶标,即将其中的\(x\)点顶标\(-d\)\(y\)点顶标\(+d\)。为保速度,\(d=\min\{wx_u+wy_v-w_{u,v}\}\)\(u\)在交错树中,\(v\)不在交错树中)。
  • 由于我们要尝试为左部\(n\)个点匹配,每次匹配最多增广\(n\)次(即最多要修改\(n\)次顶标,因为无法保证修改完一次顶标后就能扩大相等子图),每次增广是\(O(n+m)\)的,故此做法的复杂度应为\(O(n^2(n+m))\)

某个优化

  • 给每个\(y\)顶点一个“松弛量”函数\(slack\),每次开始找增广路时初始化为无穷大。在寻找增广路的过程中,检查边<i,j>时,如果它不在相等子图中,则让\(slack[j]\)变成原值与\(w_{i,j}\)的较小值。这样,在修改顶标时,取所有不在交错树中的\(y\)顶点的\(slack\)值中的最小值作为\(d\)值即可。但还要注意一点:修改顶标后,要把所有的不在交错树中的\(y\)顶点的\(slack\)值都减去\(d\)
  • 这个优化似乎是很有用,但并不能把KM优化到\(O(n^3)\)。这其实和原算法差不多,还是要为左部\(n\)个点匹配,每次匹配还是最多要增广\(n\)次,每次增广还是\(O(n+m)\)。如果是完全图,并且出题人稍微构造一下数据,依然是\(O(n^4)\)

    Code
bool dfs(int k) {
    visx[k] = 1;
    F(i, 1, n) {
        if (!visy[i]) {
            int t = A[k] + B[i] - Edge[k][i];
            if (!t) {
                visy[i] = 1;
                if (!link[i] || dfs(link[i])) return link[i] = k;
            } else slack[i] = min(slack[i], t);
        }
    }
    return 0;
}

int KM() {
    mem(link, 0);
    F(i, 1, n) {
        A[i] = -1e18, B[i] = 0;
        F(j, 1, n)
            A[i] = max(A[i], Edge[i][j]);
    }
    F(v, 1, n) {
        int cnt = 0;
        F(i, 1, n) slack[i] = 1e18;
        while (1) {
            mem(visx, 0), mem(visy, 0);
            if (dfs(v)) break;
            int d = 1e18;
            F(i, 1, n) if (!visy[i]) d = min(d, slack[i]);
            F(i, 1, n) if (visx[i]) A[i] -= d;
            F(i, 1, n) if (visy[i]) B[i] += d; else slack[i] -= d;
        }
    }
    Ans = 0;
    F(i, 1, n) Ans += A[i] + B[i];
    return Ans;
}

再次优化

  • 先前的算法中,我们把大量时间浪费在 修改顶标-尝试增广 的操作上了。每次修改完顶标后,我们都要花至多\(O(n^2)\)的时间走先前已经走过的路。
  • 但实际上,每次修改顶标后,我们可以确定一个\(y\)点可以被增广:那就是迫使我们修改顶标的那个\(y\)点。我们可以记录下它,并且下次增广就直接从它已连的\(x\)点增广(当然,如果它没有连\(x\)点,那就增广结束)。
  • 这样,我们就把\(dfs\)的增广改为了一个类似\(bfs\)的东西。并且对于每个\(x\)点而言,每次修改顶标后不需要清空\(vis\)数组、增广时每个点、每条边至多被经过一次,故时间复杂度成功优化至\(O(n^2+nm)\)

    Code
void augment(int s)
{
    mem(vis,0), mem(slack,63);
    int y0,nxt=0,tm;
    for(my[0]=s; vis[y0=nxt]=1,my[y0];)
    {
        int x=my[y0],d=INF;
        fo(y,1,n)
            if(!vis[y])
            {
                if((tm=wx[x]+wy[y]-w[x][y])<slack[y]) slack[y]=tm,pre[y]=y0;
                if(slack[y]<d) d=slack[y],nxt=y;
            }
        if(d) fo(y,0,n) vis[y]?wx[my[y]]-=d,wy[y]+=d:slack[y]-=d;
    }
    for(int y; y0; y0=pre[y=y0],my[y]=my[y0]);
}

int KM()
{
    mem(wy,0);
    fo(i,1,n)
    {
        wx[i]=0;
        fo(j,1,n)
        {
            if(wx[i]<w[i][j]) wx[i]=w[i][j];
            if(wy[j]<w[i][j]) wy[j]=w[i][j];
        }
        my[i]=0;
    }
    fo(i,1,n*n) augment(i);
    s=0;
    fo(i,1,n*n) s+=wx[i]+wy[i];
    return s;
}

小结

  • 学算法时要带着脑子,莫被网上的博客骗了。

转载于:https://www.cnblogs.com/Iking123/p/11300885.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值