一張圖說清匈牙利算法(Hungarian Algorithm)

做多目標(biāo)跟蹤的時候會碰到這個算法,每個人都有自己的說法講清楚這個算法是干什么的?我的老師就跟我說過是什么給工人分配活干(即理解為指派問題),網(wǎng)上還看到有說紅娘盡可能匹配多的情侶等,透過這些感性理解,基本上就能理解大概是最大匹配的問題了。

然后加了限制:后來者優(yōu)先。即后匹配的搶掉前人已匹配的對象,這個是有數(shù)學(xué)依據(jù)還是只是一種實(shí)現(xiàn)思路我就沒深究了。

我的理解不會比別人更高級,之所以能用一張圖說清楚,只不過是我作圖的時候發(fā)現(xiàn)可以把過程畫在一張圖里,只需要把圖示標(biāo)清楚就好了,這樣就不需要每一步畫一張圖了,一旦理解了,哪怕忘了,一瞅這張圖也能立刻回憶起來。

先上數(shù)據(jù):

import numpy as np

relationship_matrix = np.array([
    [1,1,0,1,0,0,0],
    [0,1,0,0,1,0,0],
    [1,0,0,1,0,0,1],
    [0,0,1,0,0,1,0],
    [0,0,0,1,0,0,0],
    [0,0,0,1,0,0,0]
], dtype=bool)

你可以理解為6個工人,7個工作,6個男孩,7個女孩等,當(dāng)然,6行7列,這么直觀理解也是一點(diǎn)問題都沒有的。

算法匹配過程如下:


hungarian_algorithm.png
  • 灰藍(lán)線就是被搶掉的
  • 綠線就是搶奪失敗的
  • 紫線是被搶了后找候選成功的
  • 紅線是一次性成功的

其中被搶的和搶奪失敗的還加了刪除線,這是為了強(qiáng)調(diào)。匹配成功的就是紅線紫線,也就是說,我們匹配出來的是:

[0,1], [1,4], [2,0], [3,2], [4,3]

甚至可以這么表示這個過程:

x0,y0
x1,y1
x2,y0 -> x0,y1 -> x1->y4 (x2搶x0的,x0搶x1的)
x3,y2
x4,y3
x5,y3 -> x4匹配不到新的,搶奪失敗,-> x5,null

有沒有說清楚?就兩步:

  1. 根據(jù)關(guān)聯(lián)表直接建立關(guān)系
  2. 如果當(dāng)前C匹配的對象已經(jīng)被B匹配過了,那么嘗試把它搶過來:
  • B去找別的匹配
    • 找到了(A)就建立新的匹配
      • 如果新的匹配(A)也已經(jīng)被別人(D)匹配了,那么那個“別人(D)”也放棄當(dāng)前匹配去找別的(遞歸警告
    • 如果找不到新的匹配,那么C搶奪失敗,遞歸中的D也同理,失敗向上冒泡

注意遞歸怎么寫代碼就能寫出來了:

nx, ny = relationship_matrix.shape    # 6個x,7個y

# 如果x0與y0關(guān)聯(lián),x3也與y0關(guān)聯(lián),那么x0去找新的匹配時,需要把y0過濾掉
# 同理x0如果找到下一個y2,y2已被x2關(guān)聯(lián),那么x2找新的匹配時[y0, y2]都需要過濾掉
# 我們把這個數(shù)組存為y_used
y_used = np.zeros((ny,), dtype=bool)  # 存y是否連接上
path = np.full((ny,), -1, dtype=int)  # 存x連接的對象,沒有為-1

def find_other_path_and_used(x):
    for y in range(ny):
        if relationship_matrix[x, y] and not y_used[y]:
            y_used[y] = True        # 處于爭奪中的y,需要打標(biāo),在后續(xù)的遞歸時要過濾掉
            if path[y] == -1 or find_other_path_and_used(path[y]):
                path[y] = x         # 直接連接 和 搶奪成功
                return True
    return False                    # 搶奪失敗 和 默認(rèn)失敗

for x in range(nx):
    y_used[:] = False  # empty
    find_other_path_and_used(x)

for y, x in enumerate(path):
    if x != -1:
        print(x, y)

真的寫代碼實(shí)現(xiàn)的時候,難點(diǎn)反而是y_used這個,第一遍代碼沒考慮這一點(diǎn),導(dǎo)致遞歸的時候每次都從y_0開始而出現(xiàn)死循環(huán),意識到后把處于爭搶狀態(tài)中的y打個標(biāo)就好了。

scipy中有一個算法實(shí)現(xiàn)了Hungarian algorithm:

from scipy.optimize import linear_sum_assignment

# relationship_matrix是代價矩陣
# 所以我們要代價越小越好,就用1來減
rows, cols = linear_sum_assignment(1-relationship_matrix) 
list(zip(rows, cols))
[(0, 0), (1, 1), (2, 6), (3, 2), (4, 3), (5, 4)]

為什么與上面不一樣呢?

  1. (0,0),(1,1)的匹配顯然不是我們實(shí)現(xiàn)的后來者優(yōu)先
  2. 他把行看成是工人,列看成是任務(wù),每個工人總要分配個任務(wù),所以(5,4)這種代價矩陣?yán)餂]有的關(guān)聯(lián)它也做出來了,目的只是讓“總代價”最小
(1-relationship_matrix)[rows, cols]  # 總代價為1
array([0, 0, 0, 0, 0, 1])

從它的名字也能看出來,它是理解為指派問題的(assignment)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容