匈牙利算法及KM算法详解

1 二分图相关概念

二分图定义:
二分图又称双分图、二部图、偶图,指顶点可以分成两个不相交的集U和V(U和V皆为独立集(Independent Sets)),使得在同一个集内的顶点不相邻(没有共同边)的图。
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

我们定义匹配点、匹配边、未匹配点、非匹配边。如图3中,1、4、5、7为匹配点,其他顶点为未匹配点;1-5、4-7为匹配边、其他边为非匹配边。

**匹配(matching)😗*二分图的一个“匹配”是指一些边的集合,任意两条边没有公共点。例如,图3、图4中红色的边就是图2的匹配。

**最大匹配(maximum matching):**二分图的“最大匹配”,值的是二分图的所有匹配中边数最多的匹配。图4是一个最大匹配。它包含4条匹配边。

**完美匹配(perfect matching):**二分图的一个“完美匹配”,是指所有点都在这个匹配中的一个匹配。也就是说这个匹配里的所有边刚好经过所有点一次。图4是一个完美匹配,显然,完美匹配一定是最大匹配(完美匹配的任何一个点都已经匹配,添加一条新的匹配边一定会与已有的匹配边冲突)。
但并非每个图都存在完美匹配。

2 匈牙利算法求解无权二分图最大匹配

2.1 相关概念

在这里插入图片描述
**交替路:**从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。

**增广路:**从一个未匹配点出发,走交替路,如果以另一个未匹配点(出发的点不算)为结尾,则这条交替路称为增广路(agumenting path)。例如,图 5 中的一条增广路如图 6 所示(图中的匹配点均用红色标出):
在这里插入图片描述
增广路有一个重要特点:非匹配边比匹配边多一条。因此,研究增广路的意义是改进匹配。只要把增广路中的匹配边和非匹配边的身份交换即可。由于中间的匹配节点不存在其他相连的匹配边,所以这样做不会破坏匹配的性质。交换后,图中的匹配边数目比原来多了 1 条。

我们可以通过不停地找增广路来增加匹配中的匹配边和匹配点。找不到增广路时,达到最大匹配(这是增广路定理)。匈牙利算法正是这么做的。

**增广路定理:**任意一个非最大匹配的匹配一定存在增广路。

匈牙利树一般由BFS构造(类似于BFS树)。从一个未匹配点出发运行BFS(唯一的限制是,必须走交替路),直到不能再扩展为止。例如,由图7,可以得到如图8的一棵 BFS 树:
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
这棵树存在一个叶子节点为非匹配点(7 号),但是匈牙利树要求所有叶子节点均为匹配点,因此这不是一棵匈牙利树。如果原图中根本不含 7 号节点,那么从 2 号节点出发就会得到一棵匈牙利树。这种情况如图 9 所示(顺便说一句,图 8 中根节点 2 到非匹配叶子节点 7 显然是一条增广路,沿这条增广路扩充后将得到一个完美匹配)。

2.2 算法原理

注意前面增广路的定义:“从一个未匹配点出发,走交替路,以另一个未匹配点为结尾”,首尾都是未匹配点,说明首尾的边都是非匹配边。而又是交替路,也就是说非匹配边比匹配边多一条。那么我们完全可以把这条增广路里的匹配边和非匹配边互换(称为“交换匹配”),那么匹配边就会多出 1 条,实现了“增广”的意义。并且这样做并不会对其他边造成影响,也不破坏二分图的性质。

那么我们就可以一直找增广路,不断交换匹配。根据增广路定理,如果找不到了,就说明已经达到最大匹配。

同样可以证明,已经匹配的点永远不会退出匹配,只会更换匹配。

这就是匈牙利算法最核心的部分了:一直找增广路,不断交换匹配。

可能看完上面的叙述,还是有点困惑。一直找增广路,不断交换匹配到底应该怎么做?以下我举一个便于理解例子:
在这里插入图片描述
现在Boys和Girls分别是两个点集,里面的点分别是男生和女生,边表示他们之间存在“暧昧关系”。最大匹配问题相当于,假如你是红娘,可以撮合任何一对有暧昧关系的男女,那么你最多能成全多少对情侣?(数学表述:在二分图中最多能找到多少条没有公共端点的边)

现在我们来看看匈牙利算法是怎么运作的:

我们从B1看起(男女平等,从女生这边看起也是可以的),他与G2有暧昧,那我们就先暂时把他与G2连接(注意这时只是你作为一个红娘在纸上构想,你没有真正行动,此时的安排都是暂时的)。
在这里插入图片描述
来看B2,B2也喜欢G2,这时G2已经“名花有主”了(虽然只是我们设想的),那怎么办呢?我们倒回去看G2目前被安排的男友,是B1,B1有没有别的选项呢?有,G4,G4还没有被安排,那我们就给B1安排上G4。
在这里插入图片描述
我们来细看这一过程:

开始是B1——G2;

由于B2的加入,有增广路G4——B1——G2——B2;

然后交换匹配,成为G4——B1——G2——B2;

这是不是正是前面提到的一直找增广路,不断交换匹配。

我们继续,B3直接配上G1就好了,这没什么问题。至于B4,他只钟情于G4,G4目前配的是B1。B1除了G4还可以选G2,但是呢,如果B1选了G2,G2的原配B2就没得选了。我们绕了一大圈,发现B4只能注定单身了,可怜。(其实从来没被考虑过的G3更可怜)
最终结果:
在这里插入图片描述

2.3 匈牙利算法代码

//---------------------DFS---------------------------------  
#include<iostream>  
#include<memory.h>  
using namespace std;  
  
#define MAXN 10  
int graph[MAXN][MAXN];  
int match[MAXN];  
int visitX[MAXN], visitY[MAXN];  
int nx, ny;  
  
bool findPath( int u )  
{  
    visitX[u] = 1;  
    for( int v=0; v<ny; v++ )  
    {  
        if( !visitY[v] && graph[u][v] )  
        {  
            visitY[v] = 1;  
            if( match[v] == -1 || findPath(match[v]) )  
            {  
                match[v] = u;  
                return true;  
            }  
        }  
    }  
    return false;  
}  
  
int dfsHungarian()  
{  
    int res = 0;  
    memset( match, -1, sizeof(match) );  
    for( int i=0; i<nx; i++ )  
    {  
        memset( visitX, 0, sizeof(visitX) );  
        memset( visitY, 0, sizeof(visitY) );  
        if( findPath(i) )  
            res++;  
    }  
    return res;  
}  
  
//-----------------------------BFS-------------------------------  
#include<iostream>  
#include<memory.h>  
using namespace std;  
  
#define MAXN 10  
int graph[MAXN][MAXN];  
//在bfs中,增广路径的搜索是一层一层展开的,所以必须通过prevX来记录上一层的顶点  
//chkY用于标记某个Y顶点是否被目前的X顶点访问尝试过。  
int matchX[MAXN], matchY[MAXN], prevX[MAXN], chkY[MAXN];  
int queue[MAXN];  
int nx, ny;  
  
int bfsHungarian()  
{  
    int res = 0;  
    int qs, qe;  
    memset( matchX, -1, sizeof(matchX) );  
    memset( matchY, -1, sizeof(matchY) );  
    memset( chkY, -1, sizeof(chkY) );  
  
    for( int i=0; i<nx; i++ )  
    {  
        if( matchX[i] == -1 )   //如果该X顶点未找到匹配点,将其放入队列。  
        {  
            qs = qe = 0;  
            queue[qe++] = i;  
            prevX[i] = -1;  //并且标记,它是路径的起点  
            bool flag = 0;  
  
            while( qs<qe && !flag )  
            {  
                int u = queue[qs];  
                for( int v=0; v<ny&&!flag; v++ )  
                {  
                    if( graph[u][v] && chkY[v]!=i ) //如果该节点与u有边且未被访问过  
                    {  
                        chkY[v] = i;    //标记且将它的前一个顶点放入队列中,也就是下次可能尝试这个顶点看能否为它找到新的节点  
                        queue[qe++] = matchY[v];  
                        if( matchY[v] >= 0 )  
                            prevX[matchY[v]] = u;  
                        else    //到达了增广路径的最后一站  
                        {  
                            flag = 1;  
                            int d=u, e=v;  
                            while( d!=-1 )  //一路通过prevX找到路径的起点  
                            {  
                                int t = matchX[d];  
                                matchX[d] = e;  
                                matchY[e] = d;  
                                d = prevX[d];  
                                e = t;  
                            }  
                        }  
                    }  
                }  
                qs++;  
            }  
            if( matchX[i] != -1 )  
                res++;  
        }  
    }  
    return res;  
}  

3 KM算法求解加权二分图最优匹配

3.1 相关概念

完备匹配:定义 设G=为二部图,|V1|≤|V2|,M为G中一个最大匹配,且|M|=|V1|,则称M为V1到V2的完备匹配。也就是说把一个集合中的点全部匹配到另一个集合中。在上述定义中,若|V2|=|V1|,则完备匹配即为完美匹配,若|V1|最大匹配。

二分图最优匹配:对于二分图的每条边都有一个权(非负),要求一种完备匹配方案,使得所有匹配边的权和最大,记做最优完备匹配。(特殊的,当所有边的权为1时,就是最大完备匹配问题)

二分图带权匹配与最优匹配:什么是二分图的带权匹配?二分图的带权匹配就是求出一个匹配集合,使得集合中边的权值之和最大或最小,这个匹配集合比一定是完备匹配。而二分图的最优匹配则一定为完备匹配,在此基础上,才要求匹配的边权值之和最大或最小。二分图的带权匹配与最优匹配不等价,也不互相包含。

顶标:每个节点与另一个集合中节点之间的最大权值

可行顶标(标杆):对于原图中的任意一个节点,给定一个函数 求出节点的顶标值。我们用数组 记录集合 中的节点顶标值,用数组 记录集合 中的节点顶标值。并且,对于原图中任意一条边 ,都满足 。

相等子图:设 G(V,E) 为二部图, G’(V,E’) 为二部图的子图。如果对于 G’ 中的任何边 满足, ,我们称 G’(V,E’) 为 G(V,E) 的等价子图或相等子图(是G的生成子图)。

3.2 核心思想

对二分图G和一组可行标,满足可行标边界条件(lx[i]+ly[j]=w[i,j])的所有边构成的生成子图(需要包含所有顶点),称为其等价子图(相等子图),在这个等价子图上,寻找其完备匹配,如果完备匹配存在,则这个完备匹配M就是图G的最大权匹配,最大权等于所有可行标的和; 如果完备匹配不存在,则修改可行标,用贪心的思想,将最优的边加入等价子图. KM算法就是一种逐次修改可行顶标的方法,使之对应的等价子图逐次增广(增加边),最后出现完备匹配.

3.3 KM代码示例

/******************************************************
二分图最佳匹配 (kuhn munkras 算法 O(m*m*n)).
邻接矩阵形式 。  返回最佳匹配值,传入二分图大小m,n
邻接矩阵 mat ,表示权,match1,match2返回一个最佳匹配,为匹配顶点的match值为-1,
一定注意m<=n,否则循环无法终止,最小权匹配可将全职取相反数。
初始化:  for(i=0;i<MAXN;i++)
             for(j=0;j<MAXN;j++) mat[i][j]=-inf;
对于存在的边:mat[i][j]=val;//注意不能负值 
********************************************************/
#include<string.h>
#define MAXN 310
#define inf 1000000000 
#define _clr(x) memset(x,-1,sizeof(int)*MAXN)
int KM(int m,int n,int mat[][MAXN],int *match1,int *match2)
{
        int s[MAXN],t[MAXN],l1[MAXN],l2[MAXN];
    int p,q,i,j,k,ret=0;
    for(i=0;i<m;i++)
    {
        l1[i]=-inf;
        for(j=0;j<n;j++)
            l1[i]=mat[i][j]>l1[i]?mat[i][j]:l1[i];
        if(l1[i]==-inf)  return -1;
    } 
    for(i=0;i<n;i++)
        l2[i]=0;
    _clr(match1);
    _clr(match2);
    for(i=0;i<m;i++)
    {
        _clr(t);
        p=0;q=0;
        for(s[0]=i;p<=q&&match1[i]<0;p++)
        {
            for(k=s[p],j=0;j<n&&match1[i]<0;j++)
            {
                if(l1[k]+l2[j]==mat[k][j]&&t[j]<0)
                {
                    s[++q]=match2[j];
                    t[j]=k;
                    if(s[q]<0)
                    {
                        for(p=j;p>=0;j=p)
                        {
                            match2[j]=k=t[j];
                            p=match1[k];
                            match1[k]=j;
                        }    
                    }    
                }    
            }    
        } 
        if(match1[i]<0)
        {
            i--;
            p=inf;
            for(k=0;k<=q;k++)
            {
                for(j=0;j<n;j++)
                {
                    if(t[j]<0&&l1[s[k]]+l2[j]-mat[s[k]][j]<p)
                       p=l1[s[k]]+l2[j]-mat[s[k]][j];
                }    
            }  
            for(j=0;j<n;j++)
               l2[j]+=t[j]<0?0:p;
            for(k=0;k<=q;k++)
               l1[s[k]]-=p;  
        }       
    } 
    for(i=0;i<m;i++)
        ret+=mat[i][match1[i]];
    return ret;      
}

给定一个二分图G,M为G边集的一个子集,如果M满足当中的任意两条边都不依附于同一个顶点,则称M是一个匹配。

极大匹配(Maximal Matching)是指在当前已完成的匹配下,无法再通过增加未完成匹配的边的方式来增加匹配的边数。最大匹配(maximum matching)是所有极大匹配当中边数最大的一个匹配。选择这样的边数最大的子集称为图的最大匹配问题。

如果一个匹配中,图中的每个顶点都和图中某条边相关联,则称此匹配为完全匹配,也称作完备匹配。完美匹配:如果所有点都在匹配边上,称这个最大匹配是完美匹配。

求二分图最大匹配可以用最大流(Maximal Flow)或者匈牙利算法(Hungarian Algorithm)

如果G为加权二分图,则权值和最大的完备匹配称为最佳匹配,求一个二分图的最佳匹配的普遍算法是KM(Kuhn-Munkres)算法。

KM算法的基本思想是,把权值转化为可行顶标,再用匈牙利算法求出一组完备匹配,如果无法求出完备匹配,则修改可行顶标,直至找到完备匹配为止,这时的完备匹配为最佳匹配。

Kuhn-Munkras算法流程:

(1)初始化可行顶标的值

(2)用匈牙利算法寻找完备匹配

(3)若未找到完备匹配则修改可行顶标的值

(4)重复(2)(3)直到找到相等子图的完备匹配为止

参考文献:
[1]https://blog.csdn.net/weixin_29903713/article/details/112778495
[2]https://blog.csdn.net/weixin_43093481/article/details/84558029
[3]https://www.cnblogs.com/kuangbin/archive/2012/08/19/2646535.html
[4]https://www.cnblogs.com/logosG/p/logos.html
[5]https://my.oschina.net/husthang/blog/840806?tdsourcetag=s_pcqq_aiomsg
[6]https://blog.csdn.net/qq_25379821/article/details/83750678

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zack_Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值