注:昨天刚刚看了关于python的关于数组的简单操作,就将匈牙利算法用python实现了以下。其中可能有很多点可以用python中数组本身属性实现,但由于初学,所以不熟悉而导致步骤繁琐的望指出~
1.匈牙利算法的简单例子
(1)矩阵所表示的就是从A点到B所要付出的代价,一般目标函数都是使得代价最小,那么匈牙利算法就是一种精确算法,求解在多个出发点和多个目标点的情况下得出最小代价。约束是一个出发点只能对应一个目标点,在操作矩阵上的表现为某行某列只能选择一个数,即基于所选择的数画十字,这个十字上没有其他任何对应选择。
(2)初始矩阵
[12 7 9 7 9]
[ 8 9 6 6 6]
[ 7 17 12 14 9]
[15 14 6 11 10]
[ 4 10 7 10 9]
(3)矩阵每行每列都减去该行该列的最小元素(此处每行每列至少出现一个0)
[ 5 0 2 0 2]
[ 2 3 0 0 0]
[ 0 10 5 7 2]
[ 9 8 0 5 4]
[ 0 6 3 6 5]
(4)制定完全分配方案(即每个目标每个地点都被匹配)
[ 5 0 2 0 2]
[ 2 3 “0“ 0 0]
[ (0) 10 5 7 2]
[ 9 8 (0) 5 4]
[ “0“ 6 3 6 5]
[ 5 (0) 2 “0“ 2]
[ 2 3 “0“ (0) “0“]
[ (0) 10 5 7 2]
[ 9 8 (0) 5 4]
[ “0“ 6 3 6 5]
[ 5 (0) 2 “0“ 2]
[ 2 3 “0“ (0) “0“]
[ (0) 10 5 7 2]
[ 9 8 (0) 5 4]
[ “0“ 6 3 6 5] ☑️
b.在已☑️的行中对所有有0元素的列打☑️
c.在对已☑️的列中对已有标记()的行进行☑️,得
[ 5 (0) 2 “0“ 2]
[ 2 3 “0“ (0) “0“]
[ (0) 10 5 7 2] ☑️
[ 9 8 (0) 5 4]
[ “0“ 6 3 6 5] ☑️
☑️
d.重复b和c步骤,直到不能打勾为止
e.对未☑️的行 和 已☑️的列 划去元素, 得
[ ]
[ 10 5 7 2] ☑️
[ ]
[ 6 3 6 5] ☑️
☑️
f.在剩余元素中找出最小元素,本例中为2,并对已☑️的行的每个元素进行减去最小元素的操作,
[[ 5 0 2 0 2]
[ 2 3 0 0 0]
[-2 8 3 5 0]
[ 9 8 0 5 4]
[-2 4 1 4 3]]
g.将出现负数的列在加上之前的最小值使得=负数变为0,得
[[ 7 0 2 0 2]
[ 4 3 0 0 0]
[ 0 8 3 5 0]
[ 11 8 0 5 4]
[ 0 4 1 4 3]]
e.如此反复,直到能作出完全分配为止
[ 7 (0) 2 0 2]
[ 4 3 0 (0) 0]
[ 0 8 3 5 (0)]
[ 11 8 (0) 5 4]
[ (0) 4 1 4 3]
所以本例最优解为32(0对应原矩阵位置元素之和)
2.代码
import numpy as np
# 行归约
def smallizeRow(p, dim):
min_row = np.zeros(dim)
for i in range(0, dim):
min_row[i] = min(p[i, :])
for i in range(0, dim):
p[i, :] = p[i, :] - min_row[i]
# 列归约
def smallizeCol(p, dim):
min_col = np.zeros(dim)
for i in range(0, dim):
min_col[i] = min(p[:, i])
for i in range(0, dim):
p[:, i] = p[:, i] - min_col[i]
# 计算每行每列的0元素的个数
def countZero(p, row, col, dim):
for i in range(0, dim):
for j in range(0, dim):
if( p[i, j] == 0) :
row[i,0] = row[i, 0] + 1;
col[0, j] = col[0, j] + 1;
# 对0元素进行标记
def markZero(p, row, col, visited, dim):
# 检查行
for i in range(0, dim):
if(row[i, 0] == 1):
# 若该元素为0 且 未被圆括号标记 且未被双引号标记 再进行访问操作
if(p[i ,j] == 0 and visited[i, j] != 1 and visited[i, j] != -1):
visited[i, j] = 1;
row[i, 0] -= 1
col[0, j] -= 1
for m in range(0, dim):
if(p[m, j] == 0 and visited[m, j] != 1 and visited[m, j] != -1):
visited[m, j] = -1
row[m, 0] -= 1
col[0, j] -= 1
# 检查列
for j in range(0, dim):
if(col[0, j] == 1):
for i in range(0, dim):
if(p[i ,j] == 0 and visited[i, j] != 1 and visited[i, j] != -1):
visited[i, j] = 1
col[0, j] -= 1
row[i, 0] -= 1
for m in range(0, dim):
if(p[i, m] == 0 and visited[i, m] != 1 and visited[i, m] != -1):
visited[i, m] = -1
row[i, 0] -= 1
col[0, m] -= 1
# 对多行多列存在两个及两个以上的为标记的0的操作
for i in range(0, dim):
if (row[i, 0] >= 2 ):
for j in range(0, dim):
if(p[i ,j] == 0 and visited[i, j] != 1 and visited[i, j] != -1):
visited[i, j] = 1;
row[i, 0] -= 1
col[0, j] -= 1
for m in range(0, dim):
if(p[m, j] == 0 and visited[m, j] != 1 and visited[m, j] != -1):
visited[m, j] = -1
row[m, 0] -= 1
col[0, j] -= 1
for n in range(0, dim):
if(p[i, n] == 0 and visited[i, n] != 1 and visited[i, n] != -1):
visited[i, n] = -1
row[i , 0] -= 1
col[0, n] -= 1
for j in range(0, dim):
if(col[0, j] >= 2):
for i in range(0, dim):
if(p[i ,j] == 0 and visited[i, j] != 1 and visited[i, j] != -1):
visited[i, j] = 1
col[0, j] -= 1
row[i, 0] -= 1
for m in range(0, dim):
if(p[i, m] == 0 and visited[i, m] != 1 and visited[i, m] != -1):
visited[i, m] = -1
row[i, 0] -= 1
col[0, m] -= 1
for n in range(0, dim):
if(p[n, j] == 0 and visited[n, j] != 1 and visited[n, j] != -1):
visited[n, j] = -1
row[n, 0] -= 1
col[0, j] -= 1
# 找出最小元素便于更新矩阵
def drawline(p, visited, marked_row, marked_col, dim):
tempmin = 10000
# 不相关元素进行标记,便于之后的最小元素的选择
drawline = np.zeros((dim, dim))
# 检查行是否有被圆括号标记的0元素
flag = np.zeros(dim)
for i in range(0, dim):
for j in range(0, dim):
if(visited[i][j] == 1):
flag[i] = 1
for i in range(0, dim):
if(flag[i] == 0):
marked_row[i, 0] =1
for m in range(0, dim):
if(p[i][m] == 0):
marked_col[0, m] = 1;
for n in range(0, dim):
if(visited[n][m] == 1):
marked_row[n, 0] = 1
for i in range(0, dim):
if marked_row[i, 0] == 0 :
drawline[i, :] = 1
if marked_col[0, i] == 1:
drawline[:, i] = 1
for i in range(0, dim):
for j in range(0, dim):
if drawline[i, j] != 1 and p[i, j]!= 0 and p[i, j] < tempmin:
tempmin = p[i, j]
return tempmin
# 更新矩阵便于第二次迭代寻找完全分配
def updata(p, marked_row, tempmin, dim):
for i in range(0, dim):
if marked_row[i] == 1:
p[i, :] = p[i, :] - tempmin
# print(p)
for i in range(0, dim):
for j in range(0, dim):
if p[i, j] < 0 :
p[:, j] = p[:, j] + tempmin
if __name__ == '__main__':
# 数组维度
dim = 5
# 原始数组
p = np.array([12, 7, 9, 7, 9, 8, 9, 6, 6, 6, 7, 17, 12, 14, 9, 15, 14, 6, 11, 10, 4, 10, 7, 10, 9])
p = p.reshape((dim, dim))
# 记录原始数组值
q = p.copy()
print("原始矩阵为:\n", p)
# 标记是否已找到完全分配
flag = 0
#行列归约
smallizeRow(p, dim)
smallizeCol(p, dim)
print("归约后矩阵为:\n", p)
while(flag == 0):
# 统计每行0的个数
row = np.zeros((dim, 1))
# 统计每列0的个数
col = np.zeros((1, dim))
# 标记0元素的被访问类型,当访问次数标记为1时,表示括号,-1为双引号
visited = np.zeros((dim, dim))
# 标记打勾的行与列
marked_row = np.zeros((dim, 1))
marked_col = np.zeros((1, dim))
# 标记是否完全分配, 当count=5时表示已完全分配
count = 0
solution = 0
countZero(p, row, col, dim)
# print(row)
# print(col)
markZero(p, row, col, visited, dim)
# print(p)
print("迭代标记矩阵为:\n", visited)
# print(row)
# print(col)
tempmin = drawline(p, visited, marked_row, marked_col, dim)
# print(marked_row)
# print(marked_col)
# print(tempmin)
updata(p, marked_row, tempmin, dim)
print("迭代后的矩阵为:\n", p)
for i in range(0, dim):
for j in range(0, dim):
if visited[i, j] == 1:
count += 1
solution += q[i, j]
if count == dim:
flag = 1
print("the best solution is : ", solution )
break
print("再次迭代求完全分配")
运行结果为:
初始矩阵为:
[[12 7 9 7 9]
[ 8 9 6 6 6]
[ 7 17 12 14 9]
[15 14 6 11 10]
[ 4 10 7 10 9]]
归约后的矩阵:
[[ 5 0 2 0 2]
[ 2 3 0 0 0]
[ 0 10 5 7 2]
[ 9 8 0 5 4]
[ 0 6 3 6 5]]
迭代后的标记矩阵为:
[[ 0. 1. 0. -1. 0.]
[ 0. 0. -1. 1. -1.]
[ 1. 0. 0. 0. 0.]
[ 0. 0. 1. 0. 0.]
[-1. 0. 0. 0. 0.]]
迭代之后的矩阵为:
[[ 7 0 2 0 2]
[ 4 3 0 0 0]
[ 0 8 3 5 0]
[11 8 0 5 4]
[ 0 4 1 4 3]]
再次迭代求最优解
迭代后的标记矩阵为:
[[ 0. 1. 0. -1. 0.]
[ 0. 0. -1. 1. -1.]
[-1. 0. 0. 0. 1.]
[ 0. 0. 1. 0. 0.]
[ 1. 0. 0. 0. 0.]]
迭代之后的矩阵为:
[[ 7 0 2 0 2]
[ 4 3 0 0 0]
[ 0 8 3 5 0]
[11 8 0 5 4]
[ 0 4 1 4 3]]
the best solution is : 32
3.思考
关于其时空复杂度分别为O(n^3), O(n), n为节点个数。在面对目标过多的情况下,效率不高,考虑贪心算法。即从第一行开始每行选择最小的数,然后划区所选数的当前行当前列,不列入选择范围,然后依次重复下面每行,获得次优解。然后对列进行同样的操作得到结果。将行列得到的结果比较选择更优解作为解。显然这样的时空复杂度为O(n),O(1),且结构和最优解接近。同以上例子的贪心结果为:32。