在使用匈牙利算法求解车辆调度问题时,采用固定矩阵大小,随机生成矩阵数值的形式来求解,发现在矩阵取到特定数值时,匈牙利算法则无法求解或者生成错误的值。
1.错误情况复现
具体出现情况复现如下:
(如果想看问题原因,可以直接跳到第2小节;解决方法见第3小节)
以3*3矩阵为例,我在运行时产生的矩阵权值为:
test = [
[8, 10, 11],
[0, 6, 11],
[3, 9, 12]]
然后根据匈牙利算法的过程进行每行减去此行的最小值,然后每列减去此列的最小值操作后,得到的矩阵(这里记作test0)为
test0 = [
[0, 0, 0],
[0, 4, 8],
[0, 4, 6]]
接下来就进行“试指派”过程,也就是“划线”寻找最小零元素的过程。
这里第一步要统计每一行中零元素的个数,从而找到最小零元素的行,从而标记得到“独立零元素”,并进行划线。这里很显然,第2行和第3行的零元素最少,均为1。
为了方便,这里假设取第2行为最少零元素的行,并标记第2行第1列的数字为“独立零元素”
然后按照同样的方法,标记第1行第2列的数字为“独立零元素”,从而完成第1次试指派。
为了直观表示指派后的情况,将独立零元素所在矩阵位置标记为1,非独立零元素所在矩阵位置标记为-1,其他元素所在位置标记为0,得到test1矩阵,如下所示:
test1 = [
[-1, 1, -1],
[1, 0, 0],
[-1, 0, 0]]
很显然,第1次试指派只标记了2个“独立零元素”,小于矩阵的维数(3),因此需要进行调整。
下一步需要将没有“独立零元素”的行进行打勾,将打勾的行中所含零元素的列进行打勾,对所有打勾的列中所含“独立零元素”的行进行打勾;
因此,打勾的行为第2行和第3行,打勾的列为第1列。
接着,对打勾的列(第1列)和没有打勾的行(第1行)进行划线,得到覆盖矩阵中所有的零元素最少的划线。然后对矩阵进行调整。(得到的矩阵为test2,并且将被线划到(不在交叉点)的数都记作0,划线交叉点的数记为1,其它数字与test0相同,以便下一步更容易理解)
test2 = [
[1, 0, 0],
[0, 4, 8],
[0, 4, 6]]
在矩阵调整中,找到未被线划到的数中最小的,这里为4。
在调整时,将没有被线划到的数减去4,将划线交叉点(这里是第1行第1列)加上4,得到新矩阵test3
test3 = [
[4, 0, 0],
[0, 0, 4],
[0, 0, 2]]
然后对新矩阵进行类似上面的试指派过程,这里不再赘述,直接展示试指派结果。(类似上面的test2矩阵,这里为test4)
test4 = [
[0, 0, 0],
[0, 0, 4],
[0, 0, 2]]
2.错误原因所在
此时就是问题的关键点所在了:
由于没被划线的数中最小的数为0,因此不会对test矩阵进行调整!
此时test4矩阵就不会进行继续更新,因此无论调度多少次,算法始终卡在一个死循环中,无法跳出循环。
3.解决方法
使用Python自带的算法进行求解,代码如下:
from numpy import array
from scipy.optimize import linear_sum_assignment
cost = ([[8, 10, 11], [0, 6, 11], [3, 9, 12]])
cost = array(cost)
row_ind, col_ind = linear_sum_assignment(cost)
# 调度方案
print(row_ind) # 开销矩阵对应的行索引
print(col_ind) # 对应行索引的最优指派的列索引
# 总权值
potential = cost[row_ind, col_ind].sum()
print("总权值:", potential)
print("-" * 60)
可以得到正确的运算结果,如下图:
[0 1 2]
[2 0 1]
总权值: 20
------------------------------------------------------------