二分图最佳完美匹配-KM算法

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(nm) (感觉可能比这个大,因为验证匹配由于要扫边所以比 O(n) 大,但每个点只会扫一次)。稀疏图效果较好。

拓展

我们会发现,如果不存在完美匹配,KM算法就无法进行(会因为找不到匹配的点卡住)。怎么办呢?其实这种情况存在“不选”这个操作,所以我们可以把不存在的边补上,边权给0,表示不选。

模板

HDU2255为例。

#include<cstdio>
#include<cstring>
#include<algorithm>
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'<ch||ch<'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;
}
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值