让我们详细解释匈牙利算法的工作原理,并通过一个简单的例子来说明正确的步骤,以便更清晰地理解。
我的理解与匈牙利算法的区别
我提出的理解如下:
1. 从未匹配的节点开始,选择第一个工人,匹配成本最小的任务。
2. 遍历所有工人,如果某个任务被多个工人匹配,保留成本最小的工人,其他工人选择下一个成本最小的任务。
3. 直到所有工人都有分配任务。
这种方法类似于贪心算法,即每一步都选择当前最优解。然而,贪心算法并不总能找到全局最优解,尤其是在有冲突时可能导致次优的总成本。
匈牙利算法则是一种系统化的方法,能够确保找到全局最优的任务分配方案。它通过矩阵变换和增广路径的方式,逐步优化分配,避免了贪心算法可能出现的局部最优问题。
匈牙利算法的基本步骤
匈牙利算法主要用于解决二分图的完美匹配问题,特别是分配问题,即将n个工人分配给n个任务,以最小化总成本。以下是其基本步骤:
1. 创建成本矩阵:构建一个n×n的矩阵,其中每个元素表示将第i个工人分配给第j个任务的成本。
2. 行缩减:
• 对每一行,找到最小的元素,并从该行的每个元素中减去这个最小值。
• 结果是每行至少有一个零。
3. 列缩减:
• 对每一列,找到最小的元素,并从该列的每个元素中减去这个最小值。
• 结果是每列至少有一个零。
4. 覆盖所有零:
• 使用最少数量的水平线和垂直线覆盖所有的零。
• 如果覆盖的线数等于n,找到最优匹配,算法结束。
• 否则,进行下一步。
5. 调整矩阵:
• 找到未被覆盖的最小元素。
• 从所有未被覆盖的元素中减去这个最小值。
• 将所有被两条线交叉覆盖的元素加上这个最小值。
• 返回步骤4。
6. 重复步骤4和5,直到找到最优匹配。
简单示例:
假设有3个工人(A, B, C)和3个任务(X, Y, Z),成本矩阵如下:
X Y Z
A 4 1 3
B 2 0 5
C 3 2 2
步骤1:创建成本矩阵
初始成本矩阵:
[ [4, 1, 3],
[2, 0, 5],
[3, 2, 2] ]
步骤2:行缩减
• 工人A的最小成本是1,减去1:
[4-1, 1-1, 3-1] => [3, 0, 2]
• 工人B的最小成本是0,减去0:
[2-0, 0-0, 5-0] => [2, 0, 5]
• 工人C的最小成本是2,减去2:
[3-2, 2-2, 2-2] => [1, 0, 0]
行缩减后的矩阵:
[ [3, 0, 2],
[2, 0, 5],
[1, 0, 0] ]
步骤3:列缩减
• 任务X的最小成本是1,减去1:
[3-1, 0, 2] => [2, 0, 2]
[2-1, 0, 5] => [1, 0, 5]
[1-1, 0, 0] => [0, 0, 0]
• 任务Y的最小成本是0,减去0:
[2, 0, 2]
[1, 0, 5]
[0, 0, 0]
• 任务Z的最小成本是0,减去0:
[2, 0, 2]
[1, 0, 5]
[0, 0, 0]
列缩减后的矩阵:
[ [2, 0, 2],
[1, 0, 5],
[0, 0, 0] ]
步骤4:覆盖所有零
使用最少数量的线覆盖所有零。这里有多个可能的覆盖方式,最少需要2条线(例如,一条水平线覆盖第三行,和一条垂直线覆盖第二列)。
因为覆盖线数(2)小于n(3),继续进行步骤5。
步骤5:调整矩阵
• 找到未被覆盖的最小元素。未被覆盖的元素为:
• 在第一行,第一列和第三列:2, 2
• 在第二行,第一列和第三列:1, 5
• 最小未覆盖元素是1。
• 从所有未覆盖的元素中减去1:
[2-1, 0, 2-1] => [1, 0, 1]
[1-1, 0, 5-1] => [0, 0, 4]
[0, 0, 0]
• 将所有被两条线交叉覆盖的元素加上1:
• 被水平线(第三行)和垂直线(第二列)交叉覆盖的元素是feature_map[2][1] = 0,加1后为1。
调整后的矩阵:
[ [1, 0, 1],
[0, 0, 4],
[0, 1, 0] ]
再次执行步骤4:覆盖所有零
使用最少数量的线覆盖所有零。这里需要3条线(例如,第一行、第二行、第三列),等于n(3),所以找到最优匹配,算法结束。
步骤6:找到最优匹配
通过标记矩阵中的零,可以找到每个工人的最佳任务分配:
• 工人A可以分配任务Y(唯一的零)。
• 工人B可以分配任务X或Y,但Y已被A占用,因此分配任务X。
• 工人C可以分配任务Z(唯一的零)。
最终分配:
• A → Y,成本为1
• B → X,成本为2
• C → Z,成本为2
总成本为1 + 2 + 2 = 5,这是最小的总成本。
总结
我之前的理解中,尝试通过逐步匹配最小成本任务来解决分配问题,这种方法类似于贪心算法。然而,贪心算法可能在局部最优的选择下导致全局次优的结果。而匈牙利算法通过行和列的缩减、覆盖零、调整矩阵等步骤,系统地找到全局最优的任务分配方案。
通过上述简单的例子,可以看到匈牙利算法如何确保找到最小总成本的分配方案,而不是仅仅依靠每一步的局部最优选择。
-------------------更新一个扩展用法---------------------------
当然,我们可以探讨如何使用匈牙利(Hungarian)算法来寻找权重最大的匹配。这实际上是一个最大权重匹配(Maximum Weight Matching)的问题,与之前讨论的最小成本匹配类似,但目标相反:最大化总权重而不是最小化总成本。
最大权重匹配简介
- 什么是最大权重匹配?
最大权重匹配旨在为二分图中的每对工人和任务分配匹配,使得分配的总权重(或利润)最大化。每个工人只能被分配给一个任务,每个任务也只能被分配给一个工人。
-
应用场景
• 资源分配:将资源分配给任务以最大化效益。
• 人员安排:将员工分配到项目上,以最大化整体生产力。
• 项目选择:选择项目组合以实现最大的总体收益。
将最大权重匹配转换为最小成本匹配
匈牙利算法原本设计用于解决最小成本匹配问题,但我们可以通过简单的转换将其用于最大权重匹配。具体方法如下:
1. 转换权重为成本:
• 找到权重矩阵中的最大值 Wmax。
• 构建一个新的成本矩阵 C,其中每个元素 C~i~ ~j~。
• 这样,原来的最大权重对应最小成本,确保匈牙利算法能找到最大权重的匹配。
2. 应用匈牙利算法:
• 使用转换后的成本矩阵运行匈牙利算法,找到最小成本的匹配。
• 该匹配对应于原权重矩阵中的最大权重匹配。
为什么这样转换有效?
通过将权重转换为成本,原本想要最大化的权重问题转化为最小化成本问题。因为较高的权重对应较低的成本,匈牙利算法在寻找最小成本匹配时,自然倾向于选择权重较高的匹配,从而实现了最大权重匹配的目标。
具体示例
让我们通过一个具体的示例,详细演示如何使用匈牙利算法找到最大权重匹配。
示例描述
假设我们有4个工人(Worker 1, Worker 2, Worker 3, Worker 4)和4个任务(Task A, Task B, Task C, Task D)。我们有以下权重矩阵,其中每个元素 Wi j 表示将第 i个工人分配给第 j 个任务的权重(例如,代表效益或利润):
[
[0.9 0.6 0 0]
[0 0.3 0.9 0]
[0.5 0.9 0 0]
[0 0 0.2 0]
]
步骤1:确定权重矩阵
首先,我们明确初始的权重矩阵:
W = [
[0.9 0.6 0 0]
[0 0.3 0.9 0]
[0.5 0.9 0 0]
[0 0 0.2 0]
]
步骤2:转换权重为成本
为了将最大权重匹配问题转换为最小成本匹配问题,我们需要找到权重矩阵中的最大值 Wmax。
计算 Wmax:= 0.9
构建成本矩阵 Ci j=Wmax - Wi j:
计算每个元素:
转换后的成本矩阵:
W = [
[0.0 0.3 0.9 0.9]
[0.0 0.6 0.0 0.9]
[0.4 0.0 0.9 0.9]
[0.0 0.9 0.7 0.9]
]
步骤3:应用匈牙利算法
现在,我们使用转换后的成本矩阵  来应用匈牙利算法,寻找最小成本的匹配,对应于原权重矩阵中的最大权重匹配。
步骤3.1:行缩减(Row Reduction)
对于每一行,找到最小的元素,并从该行的每个元素中减去这个最小值。
• 行1(Worker 1): 
• 最小值:0
• 缩减后:
• 行2(Worker 2): 
• 最小值:0
• 缩减后:
• 行3(Worker 3): 
• 最小值:0
• 缩减后:
• 行4(Worker 4): 
• 最小值:0.7
• 缩减后:
行缩减后的矩阵:

步骤3.2:列缩减(Column Reduction)
对于每一列,找到最小的元素,并从该列的每个元素中减去这个最小值。
• 列1(Task A): 
• 最小值:0
• 缩减后:
• 列2(Task B): 
• 最小值:0
• 缩减后:
• 列3(Task C): 
• 最小值:0
• 缩减后:
• 列4(Task D): 
• 最小值:0.2
• 缩减后:
列缩减后的矩阵:
[
\mathbf{C}_{\text{row & column reduced}} = \begin{bmatrix}
0 & 0.3 & 0.9 & 0.7 \
0.9 & 0.6 & 0 & 0.7 \
0.4 & 0 & 0.9 & 0.7 \
0.2 & 0.2 & 0 & 0 \
\end{bmatrix}
]
步骤3.3:覆盖所有零(Cover All Zeros)
我们需要使用最少数量的水平线(行)和垂直线(列)来覆盖矩阵中的所有零。目标是尽量少的线来覆盖所有的零。
当前矩阵中的零位置:

标记出所有的零位置:
• (1,1)
• (2,3)
• (3,2)
• (4,3), (4,4)
选择覆盖线的策略:
1. 优先选择包含最多零的行或列。
• 行1:1个零
• 行2:1个零
• 行3:1个零
• 行4:2个零
• 列1:1个零
• 列2:1个零
• 列3:2个零
• 列4:1个零
2. 选择覆盖零最多的行或列。
• 选择行4,覆盖两个零:(4,3) 和 (4,4)
覆盖线数:1(水平线覆盖行4)
3. 更新零位置(移除已被覆盖的零)。
• (4,3) 和 (4,4) 已被覆盖
• 余下的零位置:
• (1,1), (2,3), (3,2)
4. 再次选择覆盖零最多的行或列。
• 行1:1个零
• 行2:1个零
• 行3:1个零
• 列1:1个零
• 列2:1个零
• 列3:1个零
选择任意一条零元素的行或列。例如,选择列1,覆盖(1,1)
覆盖线数:2(水平线覆盖行4,垂直线覆盖列1)
5. 更新零位置(移除已被覆盖的零)。
• (1,1) 被覆盖
• 余下的零位置:
• (2,3), (3,2)
6. 再次选择覆盖零最多的行或列。
• 行2:1个零
• 行3:1个零
• 列2:1个零
• 列3:1个零
选择列3,覆盖(2,3)
覆盖线数:3(水平线覆盖行4,垂直线覆盖列1和列3)
7. 更新零位置(移除已被覆盖的零)。
• (2,3) 被覆盖
• 余下的零位置:
• (3,2)
8. 选择覆盖零最多的行或列。
• 行3:1个零
• 列2:1个零
选择列2,覆盖(3,2)
覆盖线数:4(水平线覆盖行4,垂直线覆盖列1、列3和列2)
9. 剩余未被覆盖的零:无
总结:
我们使用了4条线(1条水平线覆盖行4,3条垂直线覆盖列1、列3和列2)来覆盖所有的零。这等于矩阵的维度 ,因此,我们已经找到最优匹配,算法可以结束。
步骤3.4:找到最优匹配
既然覆盖线的数量等于矩阵的维度,我们可以通过选择不冲突的零来找到最优匹配。
选择不冲突的零:
1. 工人1:
• 零位置:(1,1)
• 分配:Worker 1 → Task A,成本 = 0
2. 工人2:
• 零位置:(2,3)
• 分配:Worker 2 → Task C,成本 = 0
3. 工人3:
• 零位置:(3,2)
• 分配:Worker 3 → Task B,成本 = 0
4. 工人4:
• 零位置:(4,3), (4,4)
• 由于Task C已被Worker 2占用,选择Task D
• 分配:Worker 4 → Task D,成本 = 0
最终分配:
• Worker 1 → Task A,成本 = 0
• Worker 2 → Task C,成本 = 0
• Worker 3 → Task B,成本 = 0
• Worker 4 → Task D,成本 = 0
总成本:0 + 0 + 0 + 0 = 0
注意:在此示例中,由于多个零元素存在,存在多种最优分配方案。例如:
• 方案1:
• Worker 1 → Task A
• Worker 2 → Task C
• Worker 3 → Task B
• Worker 4 → Task D
• 方案2:
• Worker 1 → Task A
• Worker 2 → Task C
• Worker 3 → Task B
• Worker 4 → Task D
两种方案的总成本均为0,都是最优的匹配方案。
完整匹配表
工人 任务 权重 成本
Worker 1 Task A 0.9 0
Worker 2 Task C 0.9 0
Worker 3 Task B 0.9 0
Worker 4 Task D 0 0
总成本:0
对应的总权重:0.9 + 0.9 + 0.9 + 0 = 2.7
总结
通过将最大权重匹配问题转换为最小成本匹配问题,我们成功应用了匈牙利算法来找到最优的任务分配方案,使得总权重最大化。关键步骤包括:
1. 权重转换:将权重矩阵转换为成本矩阵。
2. 应用匈牙利算法:执行行缩减、列缩减、覆盖零、调整矩阵等步骤。
3. 确定匹配:选择不冲突的零,确保每个工人分配一个唯一的任务。
重要提醒
• 矩阵必须是方阵:如果工人和任务数量不相等,可以通过添加虚拟工人或任务,使得矩阵成为方阵。
• 处理负权重:如果权重矩阵中存在负值,可以通过适当的转换(例如,加上一个常数)确保所有权重为正值,再进行转换。
• 多种最优匹配:在权重矩阵中存在多个相同权重的情况下,可能存在多种最优分配方案。
扩展阅读
• scipy.optimize.linear_sum_assignment:Python中scipy库提供了linear_sum_assignment函数,可以方便地实现匈牙利算法。
import numpy as np
from scipy.optimize import linear_sum_assignment
原始权重矩阵
W = np.array([
[0.9, 0.6, 0, 0],
[0, 0.3, 0.9, 0],
[0.5, 0.9, 0, 0],
[0, 0, 0.2, 0]
])
转换为成本矩阵
W_max = W.max()
C = W_max - W
应用匈牙利算法
row_ind, col_ind = linear_sum_assignment©
输出匹配结果
for i, j in zip(row_ind, col_ind):
print(f"Worker {i+1} -> Task {chr(65+j)} | Weight: {W[i, j]} | Cost: {C[i, j]}")
输出示例:
Worker 1 -> Task A | Weight: 0.9 | Cost: 0.0
Worker 2 -> Task C | Weight: 0.9 | Cost: 0.0
Worker 3 -> Task B | Weight: 0.9 | Cost: 0.0
Worker 4 -> Task D | Weight: 0.0 | Cost: 0.9
• 处理不等数量的工人和任务:在工人和任务数量不相等时,可以通过添加虚拟工人或任务,将矩阵扩展为方阵,并赋予虚拟任务或工人一个适当的权重(如0)来进行匹配。