1. 程式人生 > >指派問題——匈牙利Hungary演算法(用python實現)

指派問題——匈牙利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。