二分图匹配及匈牙利算法的全面讲解及python实现

二分图匹配及匈牙利算法的全面讲解及python实现1 背景在生活中常常遇到两组元素多对多匹配而又数目有限的情况 我们需要对其进行最大匹配数的分配 使效率最大化 例如 有一组压缩气缸和一组压缩活塞 每一个型号的压缩气缸有一个固定的内径大小 每一个型号的压缩活塞可以匹配内径在一定范围内的气缸 使用匈牙利算法得到活塞和气缸对大匹配数的方案 2 二分图定义二分图又称作二部图 是图论中的一种特殊模型 设 G V E 是一个无向图 如果顶点 V 可

1、背景

在生活中常常遇到两组元素多对多匹配而又数目有限的情况,我们需要对其进行最大匹配数的分配,使效率最大化。例如,有一组压缩气缸和一组压缩活塞,每一个型号的压缩气缸有一个固定的内径大小,每一个型号的压缩活塞可以匹配内径在一定范围内的气缸,使用匈牙利算法得到活塞和气缸对大匹配数的方案。

2、二分图定义

二分图又称作二部图,是图论中的一种特殊模型。 G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(ij)所关联的两个顶点ij分别属于这两个不同的顶点集(i∈ A,j∈ B),则称图G为一个二分图。选择这样的子集中边数最大的子集称为图的最大匹配问题。
二分图匹配及匈牙利算法的全面讲解及python实现

二分图匹配是很常见的算法问题,一般用匈牙利算法解决二分图最大匹配问题。

3、匹配定义

匹配:MG的一个匹配,(M{
(x2,y4),(x3,y1),(x4,y3),(x6,y5)})

M-交错路:pG的一条通路,如果p中的边为不属于M但属于G中的边与属于M中的边交替出现,则称p是一条M-交错路。即从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边、形成的路径。例如:x1-y3-x2-y3

M-饱和点:对于vV(G),如果vM中的某条边关联,则称vM-饱和点,否则称v是非M-饱和点。如x2,x3,x4,x6,y1,y3,y4,y5都属于M-饱和点,而其它点都属于非M-饱和点。

M-可增广路:p是一条M-交错路,如果p的起点和终点都是非M-饱和点,则称pM-可增广路。即从一个未匹配点出发,走交替路,途径另一个未匹配点(出发的点不算)。如:x1-y1-x3-y2

由增广路的定义可以推出下述三个结论:

    1. P的路径个数必定为奇数,第一条边和最后一条边都不属于M

    2. MP进行取反操作可以得到一个更大的匹配M’

    3. MG的最大匹配当且仅当不存在M的增广路径。

最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。

完备匹配:如果一个匹配中,|A|<=|B|且匹配数|M|=|A|则称此匹配为完全匹配,也称作完备匹配。特别的当|A|=|B|称为完美匹配

|A||B|的二分图一定没有完全匹配,正则的|A|=|B|的二分图一定有完全匹配。

4、最大匹配与最小点覆盖
最小点覆盖:假如选了一个点就相当于覆盖了以它为端点的所有边,你需要选择最少的点来覆盖所有的边

最小割定理是一个二分图中很重要的定理:一个二分图中的最大匹配数等于这个图中的最小点覆盖数。

最小点集覆盖==最大匹配的证明。首先,最小点集覆盖一定>=最大匹配,因为假设最大匹配为n,那么我们就得到了n条互不相邻的边,光覆盖这些边就要用到n个点。现在我们来思考为什么最小点集覆盖一定<=最大匹配。任何一种n个点的最小点集覆盖,一定可以转化成一个n的最大匹配。因为最小点集覆盖中的每个点都能找到至少一条只有一个端点在点集中的边(如果找不到则说明该点所有的边的另外一个端点都被覆盖,所以该点则没必要被覆盖,和它在最小点集覆盖中相矛盾),只要每个端点都选择一个这样的边,就必然能转化为一个匹配数与点集覆盖的点数相等的匹配方案。所以最大匹配至少为最小点集覆盖数,即最小点集覆盖一定<=最大匹配。综上,二者相等。

现在要用匈牙利算法找出最多能发展几对。

5、匈牙利算法

求最大匹配的一种显而易见的算法是:先找出全部匹配,然后保留匹配数最多的。但是这个算法的时间复杂度为边数的指数级函数。因此,需要寻求一种更加高效的算法。

匈牙利算法是由匈牙利数学家Edmonds1965年提出,是基于Hall定理中充分性证明的思想,它是部图匹配最常见的算法,该算法的核心就是寻找增广路径,它是一种用增广路径求二分图最大匹配的算法。如果我们置换增广路径中的匹配边和非匹配边,由于增广路径的首尾是非匹配点,其余则是匹配点,这样的置换不会影响原匹配中其他的匹配边和匹配点,因而不会破坏匹配;亦即增广路径的置换,可以得到比原有匹配更大的匹配(具体来说,匹配的边数增加了1)。

算法大致思路:M为空;

    找出一条增广路径P,通过异或操作获得更大的匹配M’代替M

    重复操作直到找不出增广路径为止

由于二分图的最大匹配必然存在(比如,上限是包含所有顶点的完全匹配),所以,再任意匹配的基础上,如果我们有办法不断地搜寻出增广路径,直到最终我们找不到新的增广路径为止,我们就有可能得到二分图的一个最大匹配。这就是匈牙利算法的核心思想。唯一的问题在于,在这种贪心的思路下,我们如何保证不存在例外的情况,即:当前匹配不是二分图的最大匹配,但已找不到一条新的增广路径。

如何快速找出增广路:如下图所示,找一个非M-饱和点,画一棵M-交错树,如果能找到,就重复(2),不能找到算法就可以结束。
二分图匹配及匈牙利算法的全面讲解及python实现

假设存在这样的情况:当前匹配不是二分图的最大匹配,但已找不到一条新的增广路径。因为当前匹配不是二分图的最大匹配,那么在两个集合中,分别至少存在一个非匹配点。那么情况分为两种:

1. 这两个点之间存在一条边:那么我们找到了一条新的增广路径,产生矛盾;

2. 这两个点之间不存在直接的边,即这两个点分别都只与匹配点相连,那么:

1) 如果这两个点可以用已有的匹配点相连,那么我们找到了一条新的增广路径,产生矛盾;

2) 如果这两个点无法用已有的匹配点相连,那么这两个点也就无法增加匹配中边的数量,也就是我们已经找到了二分图的最大匹配,产生矛盾。

综上,在所有可能的情况,上述假设都会产生矛盾。因此假设不成立,亦即贪心算法必然能求得最大匹配的解。

算法详细步骤:
              

1.任意取一个匹配M(可以是空集或者只有一条边)

              2.令S是非饱和点(尚未匹配的点)的集合

              3.如果S是空集,则M已经是最大匹配

              4.从S中取出一个非饱和点u0作为起点,从此起点走交错路P

              5.如果P是一个增广路,则令M=M⊕P=(M-P)∪(P-M)

              6.如果P都不是增广路,则从S中去掉u0,转到步骤3

5python程序

5.1 递归方法

class DFS_hungary():     # 参数初始化     def __init__(self, set_A, set_B, edge, cx, cy, visited):         self.set_A, self.set_B = set_A, set_B  # 顶点集合         self.edge = edge  # 顶点是否连边         self.cx, self.cy = cx, cy  # 顶点是否匹配         self.visited = visited  # 顶点是否被访问         self.M = []  # 匹配         self.res = 0  # 匹配数     # 遍历顶点A集合,得到最大匹配     def max_match(self):         for i in self.set_A:             if self.cx[i] == -1:  # 未匹配                 for key in self.set_B:  # 将visited置0表示未访问过                     self.visited[key] = 0                 self.res += self.path(i)                 print('i', i, 'M',self.M)     # 增广路置换获得更大的匹配     def path(self, u):         for v in self.set_B:             if self.edge[u][v] and (not self.visited[v]):  # 如果可连且未被访问过                 self.visited[v] = 1 # 访问该顶点                 if self.cy[v] == -1:  # 如果未匹配, 则建立匹配                     self.cx[u], self.cy[v] = v, u                     self.M.append((u, v))                     return 1                 else:                     self.M.remove((self.cy[v], v))  # 如果匹配则删除之前的匹配                     if self.path(self.cy[v]):  # 递归调用                         self.cx[u], self.cy[v] = v, u                         self.M.append((u, v))                         return 1             print('v', v, 'M', self.M)         return 0 if __name__ == '__main__':     set_A, set_B = ['A', 'B', 'C', 'D'], ['E', 'F', 'G', 'H']     edge = {'A': {'E': 1, 'F': 0, 'G': 1, 'H': 0}, 'B': {'E': 0, 'F': 1, 'G': 0, 'H': 1},             'C': {'E': 1, 'F': 0, 'G': 0, 'H': 1}, 'D': {'E': 0, 'F': 0, 'G': 1, 'H': 0}}  # 1表示可以匹配,0表示不能匹配     cx, cy = {'A': -1, 'B': -1, 'C': -1, 'D': -1}, {'E': -1, 'F': -1, 'G': -1, 'H': -1}     visited = {'E': 0, 'F': 0, 'G': 0, 'H': 0}     dh = DFS_hungary(set_A, set_B, edge, cx, cy, visited)     dh.max_match()     print('res', dh.res)     print('cx', cx)     print('cy', cy)     print('visited', visited) # 结果显示: # i A M [('A', 'E')]  # 对于E点,可与A点连接,第一次匹配,直接在max_match打印,存在增广路:CEAG # v E M [('A', 'E')]  # 对于E点,不能和B点连接,在path中打印 # i B M [('A', 'E'), ('B', 'F')]  # 对于F点,可与B点连接,直接在max_match打印,匹配数增加,存在增广路:CEAG # v E M [('B', 'F')]  # 对于E点,可以与C连接,但已经与A点连接,从M中移除AE,在path中打印,进入递归内部 # v F M [('B', 'F')] # i C M [('B', 'F'), ('A', 'G'), ('C', 'E')]  # 对于G点,可与A点连接,直接在max_match打印,匹配数增加,存在增广路:DGAECH # v E M [('B', 'F'), ('A', 'G'), ('C', 'E')]  # 对于E点,不能与D点连接,在path中打印 # v F M [('B', 'F'), ('A', 'G'), ('C', 'E')]  # 对于F点,不能与D点连接,在path中打印 # v E M [('B', 'F')]  # 对于G点,可以与D连接,但已经与A点连接,从M中移除AG,在path中打印,进入递归内部,继续移除CE # v F M [('B', 'F')] # v G M [('B', 'F')] # i D M [('B', 'F'), ('C', 'H'), ('A', 'E'), ('D', 'G')] # 无增广路 # res 4 # cx {'A': 'E', 'B': 'F', 'C': 'H', 'D': 'G'} # cy {'E': 'A', 'F': 'B', 'G': 'D', 'H': 'C'} # visited {'E': 1, 'F': 0, 'G': 1, 'H': 1}

5.2 非递归方法

class BFS_hungary(object):     def __init__(self, graph):         self.g = graph  # 无向图的矩阵表示         self.Nx = len(graph)  # 顶点集A的个数         self.Ny =  len(graph[0])  # 顶点集B的个数         self.Mx = [-1]*self.Nx  # 初始匹配         self.My = [-1]*self.Ny  # 初始匹配         self.chk=[-1] * max(self.Nx,self.Ny)  # 是否匹配         self.Q = []     def Max_match(self):         res=0  # 最大匹配数         self.Q = [0 for i in range(self.Nx*self.Ny)]         prev=[0] * max(self.Nx,self.Ny)  # 是否访问         for i in range(self.Nx):             if self.Mx[i]==-1:  # A中顶点未匹配                 qs=qe=0                 self.Q[qe]=i                 qe+=1                 prev[i]=-1                 flag=0                 while(qs 
  
    =0: #                                 prev[self.My[v]]=u                             else:                                 flag=1                                 d,e=u,v                                 while d!=-1:  # 将原匹配的边去掉加入原来不在匹配中的边                                     t=self.Mx[d]                                     self.Mx[d]=e                                     self.My[e]=d                                     d=prev[d]                                     e=t                     qs+=1                 if self.Mx[i]!=-1:  # 如果已经匹配                     res+=1         return res if __name__ == '__main__':     g = [[1, 0, 1, 0], [0, 1, 0, 1], [1, 0, 0, 1], [0, 0, 1, 0]]     mm = BFS_hungary(g)     print('res', mm.Max_match()) 
  

两个版本的时间复杂度均为O(V*E)。

DFS 的优点是思路清晰、代码量少,但是性能不如 BFS。测试了两种算法的性能。对于稀疏图,BFS 版本明显快于 DFS 版本;而对于稠密图两者则不相上下。

要点:

1. 从左边第 1 个顶点开始,挑选未匹配点进行搜索,寻找增广路。

1) 如果经过一个未匹配点,说明寻找成功。更新路径信息,匹配边数+1,停止搜索。

2) 如果一直没有找到增广路,则不再从这个点开始搜索。事实上,此时搜索后会形成一棵匈牙利树。

2. 由于找到增广路之后需要沿着路径更新匹配,所以我们需要一个结构来记录路径上的点。DFS版本通过函数调用隐式地使用一个栈,而BFS版本使用prev数组。

文章参考地址:

算法思路讲的很清晰:https://liam0205.me/2016/04/03/Hungarian-algorithm-in-the-maximum-matching-problem-of-bigraph/

python代码实现:https://www.cnblogs.com/jamespei/p/5555734.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/226569.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月16日 下午10:35
下一篇 2026年3月16日 下午10:35


相关推荐

  • drupal安装教程mysql_Drupal7安装完整教程

    drupal安装教程mysql_Drupal7安装完整教程Drupal7史前准备工作(安装AppServ)AppServ是PHP网页架站工具组合包,作者将一些网络上免费的架站资源重新包装成单一的安装程序,以方便初学者快速完成架站,AppServ所包含的软件有:Apache、ApacheMonitor、PHP、MySQL、phpMyAdmin等。1.下载appserv-win32-2.5.10.exe(http://www.a…

    2022年5月30日
    42
  • c字符串转数组 数组转字符串

    c字符串转数组 数组转字符串stringstr a b c d e string strArray str Split 字符串转数组 str string Empty str string Join strArray 数组转成字符串

    2026年3月18日
    1
  • unity协程简介[通俗易懂]

    unity协程简介[通俗易懂]在Unity中,一般的方法都是顺序执行的,一般的方法也都是在一帧中执行完毕的,当我们所写的方法需要耗费一定时间时,便会出现帧率下降,画面卡顿的现象。当我们调用一个方法想要让一个物体缓慢消失时,除了在Update中执行相关操作外,Unity还提供了更加便利的方法,这便是协程。在通常情况下,如果我们想要让一个物体逐渐消失,我们希望方法可以一次调用便可在程序后续执行中实现我们想要的效果。我们希望代码…

    2022年6月20日
    41
  • 近日学习笔记:df -h和du -sh命令,查看linux版本,vbm管理工具,su命令,ssh服务升级技巧,source命令

    近日学习笔记:df -h和du -sh命令,查看linux版本,vbm管理工具,su命令,ssh服务升级技巧,source命令文章目录df -h和du -sh区别df -h查看整个系统中文件系统的使用情况du -sh *查看当前目录下各个文件及目录占用空间大小查看Linux系统版本的命令:VirtualBox管理工具Vboxmanagesu与 su -的区别echo $?ssh服务升级BBS-GO的安装 linux + 宝塔面板【进了解】宝塔界面+数据库+NGINX的安装source命令~目录和/目录的区别df -h和du -sh区别(du, disk usage,-s仅显示总计,-h自适应单位 )df -h查看整个系统中文

    2022年8月11日
    8
  • 【Python入门基础】Pycharm格式化代码常用快捷键

    【Python入门基础】Pycharm格式化代码常用快捷键一常用快捷键编辑类 Ctrl D 复制选定的区域或行 Ctrl Y 删除选定的行 Ctrl Alt L 代码格式化 Ctrl Alt O 优化导入 去掉用不到的包导入 Ctrl 鼠标简介 进入代码定义 Ctrl 行注释 取消注释 Ctrl 左方括号快速跳到代码开头 Ctrl 右方括号快速跳到代码末尾 Shift F10 运行 Sh

    2026年3月17日
    2
  • 开始移动说话_按住说话怎么不能用了

    开始移动说话_按住说话怎么不能用了2019独角兽企业重金招聘Python工程师标准>>>…

    2022年4月19日
    46

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号