指派問題——匈牙利Hungary演算法(用python實現)
注:昨天剛剛看了關於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)制定完全分配方案(即每個目標每個地點都被匹配)
- 從第一行開始,依次檢查各行,直到找出只有一個未標記的0元素的一行。【圓括號表示選中標記,雙引號表示忽略標記,即如果某數上有符號,表示該數已標記】。對未標記的0元素進行圓括號選中標記,並對同一列上的其他0元素進行雙引號忽略標記。重複這一過程,直到每行沒有尚未標記的0元素或至少有2個以上的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]
- 現在,依次檢查每列。規則同上。
```
[ 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元素,則可將其中任意行或列的一個為標記的0元素作選中標記,並將同行同列的其他0元素作忽略標記。
- 以上並沒有做到完全分配(第四行還有進行選中),於是根據以下步驟調整矩陣: a.檢查尚未標記()的行,並且打勾☑️,得
[ 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。