二分图匹配之最佳匹配——KM算法

今天也大致学了下KM算法,用于求二分图匹配的最佳匹配。

何为最佳?我们能用匈牙利算法对二分图进行最大匹配,但匹配的方式不唯一,如果我们假设每条边有权值,那么一定会存在一个最大权值的匹配情况,但对于KM算法的话这个情况有点特殊,这个匹配情况是要在完全匹配(就是各个点都能一一对应另一个点)情况下的前提。

自然,KM算法跟匈牙利算法有相似之处。

其算法步骤如下:

1.用邻接矩阵(或其他方法也行啦)来储存图,注意:如果只是想求最大权值匹配而不要求是完全匹配的话,请把各个不相连的边的权值设置为0。

2.运用贪心算法初始化标杆。

3.运用匈牙利算法找到完备匹配。

4.如果找不到,则通过修改标杆,增加一些边。

5.重复3,4的步骤,直到完全匹配时可结束。

一言不合地冒出了个标杆??标杆是什么???

在解释这个问题之前,我们先来假设一个很简单的情况,用我们人类伟大的智能思维去思考思考。
在这里插入图片描述
如上的一个二分图,我们要求它的最大权值匹配(最佳匹配)

我们可以思索思索

二分图最佳匹配还是二分图匹配,所以跟和匈牙利算法思路差不多

二分图是特殊的网络流,最佳匹配相当于求最大(小)费用最大流,所以FF方法也能实现

所以我们可以把这匈牙利算法和FF方法结合起来

FF方法里面,我们每次是找最长(短)路进行通流

所以二分图匹配里面我们也找最大边进行连边!

但是遇到某个点被匹配了两次怎么办?

那就用匈牙利算法进行更改匹配!

这就是KM算法的思路了:尽量找最大的边进行连边,如果不能则换一条较大的。

所以,根据KM算法的思路,我们一开始要对边权值最大的进行连线,那问题就来了,我们如何让计算机知道该点对应的权值最大的边是哪一条?或许我们可以通过某种方式

记录边的另一端点,但是呢,后面还要涉及改边,又要记录边权值总和,而这个记录端点方法似乎有点麻烦,于是KM采用了一种十分巧妙的办法(也是KM算法思想的精髓):

添加标杆(顶标)

是怎样子呢?我们对左边每个点Xi和右边每个点Yi添加标杆Cx和Cy。

其中我们要满足Cx+Cy>=w[x][y](w[x][y]即为点Xi、Yi之间的边权值)

对于一开始的初始化,我们对于每个点分别进行如下操作

Cx=max(w[x][y]);

Cy=0;
在这里插入图片描述
然后,我们可以进行连边,即采用匈牙利算法,只是在判断两点之间是否有连线的条件下,因为我们要将最大边进行连线,所以原来判断是否有边的条件w[x][y]==0换成了

Cx+Cy==w[x][y]

此时,有一个新的名词——相等子图。

因为我们通过了巧妙的处理让计算机自动连接边权最大的边,换句话说,其他边计算机就不会连了,也就“不存在”这个图中,但我们可以随时加上这些“不存在”图中的边。此时这个图可以认为是原图的子图,并且是等效。

这样,计算机在枚举右边的点的时候,满足以上条件,就能够知道这条边是我们要连的最大的边,就能进行连边了。

于是乎我们连了AD。

接下来就尴尬了,计算机接下来要连B点的BD,但是D点已经和A点连了,怎么办呢???

根据匈牙利算法,我们做的是将A点与其他点进行连线,但此时的子图里“不存在”与A点相连的其他边,怎么办呢??

为此,我们就需要加上这些边!

很明显,我们添边,自然要加上不在子图中边权最大的边,也就是和子图里这个边权值差最小的边。

于是,我们再一度引入了一变量d,d=min{Cx[i]+Cy[j]-w[i][j]}

其中,在这个题目里Cx[i]指的是A的标杆,Cy[j]是除D点(即已连点)以外的点的标杆。

随后,对于原先存在于子图的边AD,我们将A的标杆Cx[i]减去d,D的标杆Cy[d]加上d。

这样,这就保证了原先存在AD边保留在了子图中,并且把不在子图的最大权值的与A点相连的边AE添加到了子图。

因为计算机判断一条边是否在该子图的条件是其两端的顶点的标杆满足

Cx+Cy==w[x][y]

对于原先的边,我们对左端点的标杆减去了d,对右端点的标杆加上了d,所以最终的结果还是不变,仍然是w[x][y]。

对于我们要添加的边,我们对于左端点减去了d,即Cx[i]=Cx[i]-d;为方便表示我们把更改后的的Cx[i]视为Cz[i],即Cz[i]=Cx[i]-d;

对于右端点,我们并没有对其进行操作。那这条我们要添加边的两端点的标号是否满足Cz[i]+Cy[j]=w[i][j]?

因为Cz[i]=Cx[i]-d;d=Cx[i]+Cy[j]-w[i][j];

我们把d代入左式可得Cz[i]=Cx[i]-(Cx[i]+Cy[j]-w[i][j]);

化简得Cz[i]+Cy[j]=w[i][j]。

满足了要求!即添加了新的边。

值得注意的是,这里我们只是对于一条边操作,当我们添加了几条边,要进行如上操作时,要保证原先存在的边不消失,那么我们就要先求出了d,然后

对于每个连边的左端点(记作集合S)的每个点的标号减去了d之后,然后连边的右端点(记作T)加上d,这样就保证了原先的边不消失啦~

实际上这就是一直在寻找着增广路,通过不断修改标杆进行添边实现。

接下来就继续着匈牙利算法,直到完全匹配完为止。

该算法的正确性就在于 它每次都选择最大的边进行连边

至此,我们再回顾KM算法的步骤:

1.用邻接矩阵(或其他方法也行啦)来储存图。

2.运用贪心算法初始化标杆。

3.运用匈牙利算法找到完备匹配。

4.如果找不到,则通过修改标杆,增加一些边。

5.重复3,4的步骤,直到完全匹配时可结束。

是不是清楚了许多??

因为二分图是网络流的一种特殊情况,在网络流里我们是通过不断的SPFA找到费用最大(小)的路径进行通流,跟这个有点类似。

如果我们要求边权值最小的匹配呢???

我们可以把边权值取负值,得出结果后再取相反数就可以了。

至于为什么,正负大小相反了嘛~

至此,这大概是我个人的一点点理解了,希望对您有所帮助。

若有不当之处还请大家指出QwQ。

km算法模板

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<vector>
#include<stack>
#include<queue>
#include<set>
#include<map>
#include<string>
#include<math.h>
#include<cctype>
 
using namespace std;
 
typedef long long ll;
const int maxn=550;
const int INF=(1<<29);
const double EPS=0.0000000001;
const double Pi=acos(-1.0);
 
int G[maxn][maxn];
int m;
int nx,ny;
int link[maxn],lx[maxn],ly[maxn];
int slack[maxn];
bool visx[maxn],visy[maxn];
 
bool dfs(int x)
{
    visx[x]=1;
    for(int y=0;y<ny;y++){
        if(visy[y]) continue;
        int tmp=lx[x]+ly[y]-G[x][y];
        if(tmp==0){
            visy[y]=1;
            if(link[y]==-1||dfs(link[y])){
                link[y]=x;
                return true;
            }
        }
        else if(slack[y]>tmp) slack[y]=tmp;
    }
    return false;
}
 
int KM()
{
    memset(link,-1,sizeof(link));
    memset(ly,0,sizeof(ly));
    for(int i=0;i<nx;i++){
        lx[i]=-INF;
        for(int j=0;j<ny;j++){
            if(G[i][j]>lx[i]) lx[i]=G[i][j];
        }
    }
    for(int x=0;x<nx;x++){
        for(int i=0;i<ny;i++) slack[i]=INF;
        while(1){
            memset(visx,0,sizeof(visx));
            memset(visy,0,sizeof(visy));
            if(dfs(x)) break;
            int d=INF;
            for(int i=0;i<ny;i++){
                if(!visy[i]&&d>slack[i]) d=slack[i];
            }
            for(int i=0;i<nx;i++){
                if(visx[i]) lx[i]-=d;
            }
            for(int i=0;i<ny;i++){
                if(visy[i]) ly[i]+=d;
                else slack[i]-=d;
            }
        }
    }
    int res=0;
    for(int i=0;i<ny;i++){
        if(link[i]!=-1) res+=G[link[i]][i];
    }
    return res;
}
 
int main()
{
    while(cin>>nx>>ny){
        memset(G,0,sizeof(G));
        cin>>m;
        while(m--){
            int u,v;
            scanf("%d%d",&u,&v);
            G[u][v]=1;
        }
        cout<<KM()<<endl;
    }
    return 0;
}

KM算法大概过程:

(1)初始化Lx数组为该boy的一条权值最大的出边。初始化Ly数组为 0。

(2)对于每个boy,用DFS为其找到一个girl对象,顺路记录下S和T集,并更新每个girl的slack值。若不能为其找到对象,则转3。

(3)找出非T集合的girl的最小slack值为d,更新S集中的boy和T集中的girl,并且顺路更新非T集中的slack值。对于那个失败的boy继续第2步。

简括之,就是在保持当前权和最高的情况下,尽量为每个boy找到权更大的边。找的过程就是DFS过程,标记出S和T集是为了保证权和最大,因为只要帮S中任意一个boy另找一个女对象,为这个boy的此次脱单之路告终。

DFS的要完成的任务:

(1)标记S和T集。

(2)更新每个girl的slack值为最小。
  
改自kuangbin的模板

/* KM算法:复杂度O(nx*nx*ny)
* 完全二分图求最大权匹配(必须为所有boy找到对象,且boy数量必须<=girl数量)
* 若求最小权匹配,可将权值取相反数,结果取相反数
* 点的编号从1开始。
* 以男女模型出现比较直观。
*/
int  nx, ny;                  //两边的点数,x为男,y为女。
int  g[N][N];                 //二分图描述,g[x][y]表示边权。
int  girl[N], Lx[N], Ly[N];   //girl[i]记录i的匹配成功对象,男女的顶标
int  slack[N];      //为了优化用的,连接到对应girl的松弛值。
bool S[N], T[N];    //匈牙利树的节点集合,S为男,T为女。

bool DFS(int x) // x一定是boy
{
    S[x]=true;
    for(int i=1; i<=ny; i++) //枚举girl
    {
        if(T[i]) continue;
        int tmp=Lx[x]+Ly[i]-g[x][i];
        if( tmp==0 )
        {
            T[i]=true;
            //为第i个girl的男对象另找女对象
            if(girl[i]==-1 || DFS(girl[i]))
            {
                girl[i]=x;      //记录匹配的boy
                return true;
            }
        }
        else if(slack[i]>tmp)   //顺便更新下slack
            slack[i]=tmp;
    }
    return false;
}

int KM()
{
    memset(girl, -1, sizeof(girl));
    memset(Ly, 0, sizeof(Ly));
    for(int i=1; i<=nx; i++) //初始化两个L数组分别为-INF和0
    {
        Lx[i] = -INF;
        for(int j=1; j<=ny; j++)
            if(g[i][j]>Lx[i])    Lx[i]=g[i][j];
    }
    for(int j=1; j<=nx; j++)     //枚举boy
    {
        for(int i=1; i<=ny; i++) //初始slack为无穷。slack只需要记录girl的。
            slack[i]=INF;
        while(true)     //无限循环,直到帮boy[j]找到对象
        {
            memset(S, 0, sizeof(S));
            memset(T, 0, sizeof(T));
            if( DFS(j) )  break;    //直接就找到对象了,搞定。
            int d=INF;
            for(int i=1; i<=ny; i++)    //根据不在匈牙利树上的girl的slack值找到最小值d
                if(!T[i] && d>slack[i])
                    d=slack[i];
            for(int i=1; i<=nx; i++)     //所有匈牙利树上的boy更新lx值
                if(S[i])    Lx[i]-=d;
            for(int i=1; i<=ny; i++)     //树上的girl加d,不在树上的girl的slack减d。
            {
                if(T[i])     Ly[i]+=d;   //这是为了让等式仍然成立
                else         slack[i]-=d;
            }
        }
    }
    int ans=0;
    for(int i=1; i<=ny; i++) //累计匹配边的权和
        if(girl[i]>0) ans+=g[girl[i]][i];
    return ans;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值