【算法】Hungarian Algorithm 匈牙利算法
最近在看文章时看到了匈牙利算法,之前没怎么了解过这个算法,所以查了查资料把他这个算法总结一下写在这里了
匈牙利算法是一种在多项式时间内求解指派问题的算法,其核心思想是通过构造增广路径来寻找最大匹配,进而得到最优指派方案。
1 用匈牙利算法解决指派问题
举例一个典型的指派问题:假设现在有\(n\)个学生,以及\(n\)个任务,学生\(i\)完成任务\(j\)的代价为\(c_{ij}\)(构成成本矩阵\(C\)),现在,我希望设计一种任务指派的方式,使得每个学生完成一个任务,并且总体代价最小
匈牙利算法解决上述指派问题的四个步骤:
步骤 1:行减法(Subtract row minima)
对于成本矩阵\(C\)的每一行,找出该行中的最小元素,然后从该行的每个元素中减去这个最小元素。这一步的目的是减少成本矩阵中的非零元素,使得每一行至少有一个零元素。这不会改变最优解,因为减去一个常数不会影响相对成本。
步骤 2:列减法(Subtract column minima)
类似地,对于矩阵的每一列,找出该列中的最小元素,然后从该列的每个元素中减去这个最小元素。这一步的目的是进一步减少成本矩阵中的非零元素,使得每一列至少有一个零元素。这同样不会改变最优解。
步骤 3:用最少的线覆盖所有零(Cover all zeros with a minimum number of lines)
用最少数量的水平线和垂直线覆盖经过步骤 1 和 2 处理后的矩阵中的所有零(注:这里可以用最大流问题的Ford Fulkerson算法来解决,当然也有更简单的方法,这里使用了Ford Fulkerson算法,后面将会结合最大流问题更详细地解释一下什么是增广路)这个。如果需要n条线来覆盖所有零(n是成本矩阵的维度),则在这些零中存在最优指派方案,算法结束。
如果少于n条线就能覆盖所有零,则继续执行步骤 4。
步骤 4:创建额外的零(Create additional zeros)
找出在步骤 3 中未被线覆盖的最小元素(记为k),从所有未被覆盖的元素中减去k,同时将所有被两条线覆盖的元素加上k。
然后重复步骤 3 和步骤 4,直到找到最优指派方案。
最后步骤3中给出最优指派方案的方法是:在最终的矩阵中,从每个行和列中各选择一个零,使得这些零互不重叠(即没有两个零在同一行或同一列),这些零对应的位置即为最优指派方案。具体来说,可以按照以下步骤进行:
- 从第一行开始,找到第一个未被标记的零,将其标记为选中,并将该零所在列的所有其他零标记为不可选。
- 移动到下一行,重复上述操作,直到所有行都被处理完毕。
- 对于每一列,检查是否有选中的零。如果有,则该列对应的任务被分配给了该零所在行对应的人;如果没有,则该列的任务未被分配。
这样,就可以得到一个使得总成本最小的最优指派方案。
2 最大流问题与Ford Fulkerson算法
最大流问题是图论中的一个经典优化问题,目标是在给定一个流网络(即一个带有容量的有向图)中,找到从源点 s(source) 到汇点 t(sink) 的最大流量。
2.1 最大流算法的实际例子
想象一下,你正在管理一个城市的道路网络,这个网络由若干个交叉路口(节点)和道路(边)组成。每条道路都有一个最大容量,表示在单位时间内可以通过的最大车流量。
假设你有以下道路网络:
- 源点A到交叉路口B的道路容量为10辆车/小时。
- 源点A到交叉路口C的道路容量为5辆车/小时。
- 交叉路口B到汇点D的道路容量为8辆车/小时。
- 交叉路口B到交叉路口C的道路容量为2辆车/小时。
- 交叉路口C到汇点D的道路容量为7辆车/小时。
最大流问题就是要找出在这一给定的道路容量限制下,从源点A到汇点D能够通过的最大车流量。
2.2 Ford Fulkerson算法与增广路
Ford Fulkerson算法是一种解决最大流问题的经典算法。它的核心思想是通过不断寻找从源点到汇点的增广路径来增加流量,直到无法再找到增广路径为止。
所谓增广路就是一条从源点到汇点的路径,路径上的每条道路都有剩余(道路)容量可以利用。每找到一条增广路,我们就从源点派出车辆数尽可能多的一队车流开去汇点(将这条增广路吃干抹净!),然后再寻找下一条依然有剩余容量的增广路,继续将其“吃干抹净”。直到这样“吃干抹净”所有的增广路,我们就找到了这张图的最大流量。
让我们用刚才的车流量例子来说明这个算法:
-
初始化流量:一开始,所有道路的流量都为0。
-
寻找增广路径:第一次,我们可以找到路径A -> B -> D。这条路径上的最小剩余容量是8(A到B的容量为10,B到D的容量为8)。我们将这条路径上的流量增加8。更新后的流量:A -> B: 8/10,B -> D: 8/8。
-
再次寻找增广路径:第二次,我们可以找到路径A -> C -> D。这条路径上的最小剩余容量是5(A到C的容量为5,C到D的容量为7)。我们将这条路径上的流量增加5。更新后的流量:A -> C: 5/5,C -> D: 5/7。
-
继续寻找增广路径:第三次,我们可以找到路径A -> B -> C -> D。这条路径上的最小剩余容量是2(A到B的剩余容量为2,B到C的容量为2,C到D的剩余容量为2)。我们将这条路径上的流量增加2。更新后的流量:A -> B: 10/10,B -> C: 2/2,C -> D: 7/7。
-
无法再找到增广路径:此时,我们无法再找到从源点A到汇点D的增广路径,因为所有可能的路径上的某条道路的剩余容量都为0。
-
计算最大流:最大流等于所有从源点出发的道路的流量之和,即8 + 5 + 2 = 15辆车/小时。
3 匈牙利算法示例代码
from collections import deque
import numpy as np
def bfs(graph, s, t, parent):
"""
使用广度优先搜索 (BFS) 寻找增广路径。
:param graph: 邻接矩阵表示的图,graph[u][v] 表示从节点 u 到节点 v 的剩余容量。
:param s: 源点。
:param t: 汇点。
:param parent: 用于记录路径的父节点数组。
:return: 如果找到增广路径,返回 True;否则返回 False。
"""
visited = [False] * len(graph)
queue = deque()
queue.append(s)
visited[s] = True
while queue:
u = queue.popleft()
for ind, val in enumerate(graph[u]):
if not visited[ind] and val > 0:
queue.append(ind)
visited[ind] = True
parent[ind] = u
if ind == t:
return True
return False
def ford_fulkerson(graph, source, sink):
"""
Ford-Fulkerson 算法用于计算最大流。
:param graph: 邻接矩阵表示的图,graph[u][v] 表示从节点 u 到节点 v 的剩余容量。
:param s: 源点。
:param t: 汇点。
:return: 图的最大流和每条边上的实际流量。
"""
parent = [-1] * len(graph)
max_flow = 0
while bfs(graph, source, sink, parent):
path_flow = float("Inf")
s = sink
while s != source:
path_flow = min(path_flow, graph[parent[s]][s])
s = parent[s]
max_flow += path_flow
v = sink
while v != source:
u = parent[v]
graph[u][v] -= path_flow
graph[v][u] += path_flow
v = parent[v]
return max_flow
def hungarian_algorithm(cost_matrix):
n = cost_matrix.shape[0]
# 步骤1: 减去行最小值
for i in range(n):
cost_matrix[i, :] -= np.min(cost_matrix[i, :])
# 步骤2: 减去列最小值
for j in range(n):
cost_matrix[:, j] -= np.min(cost_matrix[:, j])
# 步骤3:通过构建含有零的矩阵所对应二分图的邻接矩阵解决“用最少的线覆盖所有零”问题
graph = np.zeros((2 * n + 2, 2 * n + 2), dtype=int)
for i in range(1, n + 1):
graph[0, i] = 1 # 从源点到所有的行添加一条容量为1的路线
for i in range(n + 1, 2 * n + 1):
graph[i, 2 * n + 1] = 1 # 从所有的列到汇点添加一条容量为1的路线
for i in range(n):
for j in range(n):
if cost_matrix[i, j] == 0:
graph[i + 1, n + j + 1] = 1 # 每个零元素所在的行和列之间添加一条容量为1的路线
max_flow = ford_fulkerson(graph, 0, 2 * n + 1)
if max_flow == n:
# 找到最优指派方案,算法成功结束,计算并返回指派方案
assignment = np.zeros(n, dtype=int)
for i in range(1, n + 1):
for j in range(n + 1, 2 * n + 1):
if graph[i, j] == 0 and graph[j, i] == 1:
assignment[i - 1] = j - n - 1
return assignment
else:
# 步骤4:创建额外的零,然后重复步骤3
row_cover = np.zeros(n, dtype=bool)
col_cover = np.zeros(n, dtype=bool)
for i in range(n):
for j in range(n):
if cost_matrix[i, j] == 0 and not row_cover[i] and not col_cover[j]:
row_cover[i] = True
col_cover[j] = True
min_val = np.min(cost_matrix[~row_cover, :][:, ~col_cover])
cost_matrix[~row_cover, :] -= min_val
cost_matrix[:, col_cover] += min_val
return hungarian_algorithm(cost_matrix)
# 示例
cost_matrix = np.array(
[
[7, 514, 114, 14, 1],
[9, 12, 6, 8, 1],
[13, 4, 10, 7, 1],
[6, 8, 11, 9, 2],
[1, 2, 3, 4, 5],
]
)
original_cost_matrix = cost_matrix.copy()
assignment = hungarian_algorithm(cost_matrix)
print(f"成本矩阵{original_cost_matrix}")
print(cost_matrix)
total_cost = np.sum(
original_cost_matrix[np.arange(original_cost_matrix.shape[0]), assignment]
)
print("指派方案:")
for i, task in enumerate(assignment):
print(f"学生 {i + 1} 被指派任务 {task + 1}")
print(f"总成本: {total_cost}")
4 参考资料:
- https://zhuanlan.zhihu.com/p/677501913 匈牙利算法解决指派问题的典型案例
- https://www.hungarianalgorithm.com/hungarianalgorithm.php 匈牙利算法解决指派问题的外文网站
- https://blog.csdn.net/lemonxiaoxiao/article/details/108672039 匈牙利算法应用的初级版本——当红娘(求二分图的最大匹配数和最小点覆盖数)——事实上就是\(c_{ij}\)=0或1的更简单的指派问题
- https://blog.csdn.net/weixin_46623714/article/details/121991938 最大流算法的增广路算法和增广路的详细解释

浙公网安备 33010602011771号