文章目录
一、算法概述
- 定义:匈牙利算法(Hungarian Algorithm)用于求解二分图的最大匹配问题,即在一个二分图中,找到最多的边的集合,使得集合中的任意两条边都没有公共顶点。
- 核心思想:通过不断寻找增广路径(从非匹配点出发,交替经过非匹配边和匹配边,最终到达另一个非匹配点的路径),并对增广路径上的边进行取反操作(匹配边变为非匹配边,非匹配边变为匹配边),从而增加匹配边的数量,直到不存在增广路径为止。
- 应用场景:
- 任务分配问题:将多个任务分配给多个工人,每个工人只能执行一个任务,每个任务只能由一个工人执行,求最优分配方案。
- 资源调度问题:在资源有限的情况下,将资源分配给不同的项目,使资源利用率最大化。
- 稳定婚姻问题:在男女配对中,找到一个稳定的配对方案,使得不存在男女双方都更倾向于对方而不是当前配偶的情况。
二、算法思路
- 数据结构:
- 使用邻接表
adj
存储二分图的边信息,adj[i]
表示左边点集i
号顶点的所有边对应的右边顶点。 - 数组
pre
记录右边顶点的匹配情况,pre[i]
表示右边顶点i
匹配的左边顶点编号。 - 数组
visit
标记右边顶点是否在当前寻找增广路径过程中被访问过。
- 使用邻接表
- 初始化:
- 清空所有边信息,并将
pre
数组初始化为 -1,表示所有右边顶点初始时都未匹配。
- 清空所有边信息,并将
- 寻找增广路径:
- 从左边点集的每个顶点出发,尝试寻找增广路径。
- 对于当前顶点
u
,遍历其所有邻接的右边顶点v
,若v
未被访问过,则标记v
已访问。 - 检查
v
是否未匹配(pre[v] == -1
),若未匹配则直接将v
与u
匹配;若已匹配,则递归尝试为v
的当前匹配顶点pre[v]
寻找新的匹配。
- 更新最大匹配数:
- 每次找到一条增广路径,最大匹配数加1,直到左边点集的所有顶点都尝试完毕,最终得到二分图的最大匹配数。
三、伪代码实现
1. 数据结构与全局变量
常量 maxn = 510 # 左边点集的最大顶点数
常量 maxm = 510 # 右边点集的最大顶点数
数组 adj[maxn][] # 邻接表存储二分图的边,adj[i] 存储左边顶点 i 对应的右边顶点列表
数组 pre[maxm] # pre[i] 记录右边顶点 i 匹配的左边顶点编号
数组 visit[maxm] # visit[i] 标记右边顶点 i 是否在当前寻找增广路径过程中被访问过
变量 n # 左边点集的实际顶点数量
变量 m # 右边点集的实际顶点数量
2. 核心函数框架
算法:匈牙利算法求解二分图最大匹配
输入:二分图的左边顶点数 n,右边顶点数 m,图的边信息
输出:二分图的最大匹配数
函数 Hungarian_Init(n_, m_):
n = n_ # 初始化左边点集数量
m = m_ # 初始化右边点集数量
对 pre 数组的每个元素赋值为 -1 # 初始化右边顶点匹配情况为未匹配
for i 从 1 到 n:
清空 adj[i] # 清空左边顶点 i 的邻接边列表
函数 Hungarian_AddEdge(u, v):
将 v 添加到 adj[u] 中 # 添加从左边顶点 u 到右边顶点 v 的边
函数 Hungarian_findMatch(u):
for i 从 0 到 adj[u] 的长度 - 1:
v = adj[u][i] # 获取与左边顶点 u 邻接的右边顶点 v
if not visit[v]: # 若 v 未被访问过
visit[v] = true # 标记 v 已访问
vpre = pre[v] # 记录 v 当前匹配的左边顶点
pre[v] = u # 尝试将 v 与 u 匹配
if vpre == -1 或者 Hungarian_findMatch(vpre):
return true # 找到增广路径,返回成功
pre[v] = vpre # 恢复 v 的原匹配
return false # 未找到增广路径,返回失败
函数 Hungarian_GetMaxMatch():
cnt = 0 # 初始化最大匹配数为 0
for i 从 1 到 n:
对 visit 数组的每个元素赋值为 false # 重置访问标记
if Hungarian_findMatch(i):
cnt = cnt + 1 # 找到增广路径,匹配数加 1
return cnt # 返回最大匹配数
# 主程序(示例)
输入左边顶点数 n 和右边顶点数 m
Hungarian_Init(n, m)
输入边的数量 e
for i 从 0 到 e - 1:
输入左边顶点 u 和右边顶点 v
Hungarian_AddEdge(u, v)
输出 Hungarian_GetMaxMatch() # 输出二分图的最大匹配数
四、算法解释
1. 初始化过程(Hungarian_Init)
- 接收二分图左边和右边点集的顶点数量,分别赋值给全局变量
n
和m
。 - 将
pre
数组所有元素设为 -1,表示右边顶点初始均未匹配。 - 清空邻接表
adj
中每个左边顶点对应的边列表,为后续添加边做准备。
2. 添加边过程(Hungarian_AddEdge)
- 对于输入的边
(u, v)
,将右边顶点v
添加到左边顶点u
的邻接表adj[u]
中,用于后续寻找增广路径时遍历与u
相连的右边顶点。
3. 寻找增广路径过程(Hungarian_findMatch)
- 从左边顶点
u
出发,遍历其邻接表adj[u]
中的每个右边顶点v
。 - 若
v
未被访问过,标记v
为已访问,并记录v
当前匹配的左边顶点vpre = pre[v]
,尝试将v
与u
匹配(pre[v] = u
)。 - 若
v
未匹配(vpre == -1
),则直接找到了一条增广路径;若v
已匹配,则递归调用Hungarian_findMatch(vpre)
,尝试为v
的当前匹配顶点vpre
寻找新的匹配。若递归调用成功,则说明存在增广路径;若失败,则恢复v
的原匹配(pre[v] = vpre
)。 - 若遍历完
u
的所有邻接顶点都未找到增广路径,则返回false
。
4. 计算最大匹配数过程(Hungarian_GetMaxMatch)
- 从左边点集的第一个顶点开始,依次调用
Hungarian_findMatch
函数寻找增广路径。 - 每次调用
Hungarian_findMatch
成功找到增广路径后,将最大匹配数cnt
加1。 - 遍历完左边点集的所有顶点后,
cnt
即为二分图的最大匹配数,返回该值。
5. 示例演示
- 假设二分图左边点集有3个顶点,右边点集有3个顶点,边的连接情况为
(1, 1)
,(1, 2)
,(2, 2)
,(3, 3)
。 - 初始化后,
pre
数组元素均为 -1,adj
邻接表为空。 - 添加边后,
adj[1] = [1, 2]
,adj[2] = [2]
,adj[3] = [3]
。 - 从左边顶点1开始寻找增广路径,找到顶点1与右边顶点1匹配;继续寻找,顶点1还可与右边顶点2匹配,更新匹配;从顶点2寻找,顶点2与右边顶点2匹配;从顶点3寻找,顶点3与右边顶点3匹配。最终最大匹配数为3。
五、复杂度分析
- 时间复杂度:
- 最坏情况下,对于左边点集的每个顶点都要进行一次寻找增广路径的操作,每次寻找增广路径的过程中,需要遍历该顶点的所有邻接边,假设二分图中左边点集有
n
个顶点,右边点集有m
个顶点,边的数量为e
,则每次寻找增广路径的时间复杂度为 O ( e ) O(e) O(e),因此匈牙利算法的时间复杂度为 O ( n × e ) O(n \times e) O(n×e)。在稠密图中, e = O ( n × m ) e = O(n \times m) e=O(n×m),此时时间复杂度为 O ( n × n × m ) O(n \times n \times m) O(n×n×m);在稀疏图中, e = O ( n + m ) e = O(n + m) e=O(n+m),时间复杂度为 O ( n × ( n + m ) ) O(n \times (n + m)) O(n×(n+m)) 。
- 最坏情况下,对于左边点集的每个顶点都要进行一次寻找增广路径的操作,每次寻找增广路径的过程中,需要遍历该顶点的所有邻接边,假设二分图中左边点集有
- 空间复杂度:
- 邻接表
adj
存储图的边信息,空间复杂度为 O ( e ) O(e) O(e),其中e
为边的数量;数组pre
和visit
的长度分别为右边点集的顶点数m
,空间复杂度为 O ( m ) O(m) O(m)。因此,总的空间复杂度为 O ( e + m ) O(e + m) O(e+m),在最坏情况下, e = O ( n × m ) e = O(n \times m) e=O(n×m),空间复杂度为 O ( n × m + m ) = O ( n × m ) O(n \times m + m) = O(n \times m) O(n×m+m)=O(n×m)。
- 邻接表
本文为作者(英雄哪里出来)在抖音的独家课程《英雄C++入门到精通》、《英雄C语言入门到精通》、《英雄Python入门到精通》三个课程的配套文字讲解,如需了解算法视频课程,请移步 作者本人 的抖音直播间。