本文始發於個人公眾號:TechFlow,原創不易,求個關注
今天是演算法與資料結構專題的第31篇文章,我們一起來聊聊二分圖匹配與匈牙利演算法。
在上一篇文章當中我們介紹了一個有趣的穩定婚姻問題,模擬了男男女女配對的婚戀場景,並且研究了一下讓匹配更加穩定的Gale-Shapley演算法。如果錯過了這篇文章的同學可以從下方的傳送門回顧一下婚姻穩定問題的具體內容。
學演算法還能指導找物件?是的,這就是大名鼎鼎的穩定婚姻演算法
在上一篇文章的末尾我們曾經提到過,婚姻匹配問題本質上來說其實是二分圖匹配的問題。那麼什麼又是二分圖匹配呢?二分圖匹配的問題又該通過什麼演算法來解決呢?下面就讓我們一起從最基礎的概念開始。
二分圖匹配
二分圖的概念很簡單,就是在一個無向圖當中,所有的點可以分成兩個子集。這兩個子集當中的點各自互不相交,並且圖當中的所有邊關聯的頂點都屬於兩個不同的集合。單純用語言描述有一點吃力,其實我們找一張圖看一下就明白了。
在上圖當中很明顯左邊的豎著的三個點是一個集合,右邊豎著的三個點是另外一個集合。兩個集合之間有邊相連,集合內部互不連通。
匹配與最大匹配
在二分圖當中,如果我們選擇了一條邊就會連通對應的兩個點。這也就構成了一個匹配,我們規定一個頂點最多隻能構成一個匹配,也就是說所有的匹配之間沒有公共的點。
對於一張二分圖而言,構成的匹配數量可以是不同的,其中匹配數量最多的情況叫做最大匹配。如果所有頂點都有了匹配,那麼就稱這種情況為完美匹配。
今天要介紹的匈牙利演算法就是一種用來完成二分圖最大匹配的演算法。
匈牙利演算法
匈牙利我們都知道是一個國家的名字,這和演算法的發明人有關。匈牙利演算法的發明人Edmonds在1965年提出了匈牙利演算法,我也不知道為什麼演算法發明人是匈牙利的就叫匈牙利演算法,也沒見過其他以國家命名的演算法,是因為匈牙利人提出的演算法太少了嗎?
匈牙利演算法的核心原理非常簡單,就是尋找增廣路徑,從而達成最大匹配。
我們用通俗易懂的語言來解釋一下演算法的含義,我們還用上面那張圖作為舉例。我們首先將左邊的1和右側的a,左邊的2和右側的b節點匹配。
這樣當我們想要匹配左側的3號節點的時候發現了一個問題,那就是能夠和3號節點構成匹配的a和b節點都已經被佔據了。所以3號節點無法構成匹配,但是我們觀察一下圖就能發現,如果1和2號節點稍微調整一下匹配的情況,其實是可以給3號節點挪出一個位置來的。
具體怎麼操作呢?
我們遍歷3號節點能夠匹配的節點,首先找到a節點,發現a節點已經被佔用了。於是我們找到a節點匹配的節點也就是1號節點,試著讓它重新找一個匹配,給3號節點挪出位置來。於是我們遞迴安排1號節點,我們遍歷到b節點,發現b節點也被佔用了。於是我們同樣遞迴與b節點匹配的2號節點,看看2號節點能不能找到新的坑騰出一個位置來。
我們觀察一下發現2號節點可以和c節點構成匹配,騰出位置來給1號,這樣1號就能騰出位置來給3號節點了。所以最終的匹配結果就成了這樣:
其中藍線是調整匹配之前的結果,紅色是調整之後的結果。
本質上來說,匈牙利演算法就是一個調整匹配的過程。通過遞迴呼叫的形式去嘗試調整已經佔據了發生衝突位置的匹配,騰出位置來給右面的節點。
我們把匈牙利演算法的原理和Gale-Shapley演算法比較一下,有沒有發現什麼?其實這兩個演算法的核心原理是一樣的,在GS演算法當中我們是先由男生髮起追求,儘可能構成匹配。然後單身的男生再一輪一輪發起表白,如果有更好的匹配則斷開之前的匹配。在穩定婚姻問題當中我們定義了匹配的好壞,而在原生的二分圖匹配的問題當中匹配是不分好壞的。如果我們拋開匹配好壞不談,把優質男生搶佔劣質男生女朋友的過程看成是匹配調整的過程,那麼其實這兩個演算法的核心幾乎是一樣的。
唯一不同的是GS演算法是一輪一輪的迭代,直到所有節點完成匹配為止。因為在婚姻匹配問題當中是一定有完美匹配的解的,而二分圖匹配的問題當中,完美匹配的情況可能不一定存在。所以我們不能使用這樣迭代的方式進行,而使用遞迴進行更好一些。換句話來說匈牙利演算法研究的是二分圖匹配的通解,而GS演算法只是二分圖演算法的一個特殊案例。
程式碼實現
匈牙利演算法的思路如果學會了,程式碼其實非常簡單,就是一個簡單的遞迴呼叫。
def find_match(x):
for i in range(n):
if graph[x][i] and not tried[i]:
tried[i] = True
if match[i] == -1 or find_match(match[i]):
match[i] = x
return True
return False
for i in range(n):
tried = [0 for _ in range(n)]
find_match(i)
我們再試著用匈牙利演算法來做一下婚姻穩定問題,因為在婚姻穩定問題當中每兩個異性之間都有配對的可能,所以不需要再判斷連通的情況了。並且構成的匹配有質量好壞的差別,所以需要去掉是否嘗試過的判斷。
girls_matched = [-1 for _ in range(n)]
boys_round = [0 for _ in range(n)]
boys_matched = [-1 for _ in range(n)]
def find_match(x):
for i in range(n):
idx = girls[i].index(x)
mate = girls_matched[i]
mate_id = n+1 if mate == -1 else girls[i].index(mate)
# 如果女孩i沒有物件或者是物件比x男生弱
if mate == -1 or (idx < mate_id and find_match(girls_matched[i])):
girls_matched[i] = x
boys_matched[x] = i
return True
return False
for i in range(n):
# 對i男生進行匹配
find_match(i)
我們執行一下這段程式碼:
結果當然是正確的,但是如果我們嘗試用GS演算法演示一下會發現這兩個演算法的結果不一樣。這是為什麼呢?原因也很簡單,因為GS演算法男生追求的順序是自己喜好的順序,而匈牙利演算法當中是按照編號順序,所以因此得到的結果不同。
總結
關於匈牙利演算法的原理與介紹就到這裡結束了,對於二分圖匹配問題來說我們有很多種演算法可以解決,但是匈牙利演算法是其中比較簡單易於理解與實現的一種。如果我們將它與之前介紹的GS演算法相對比,可以發現很多共性和連通的部分。文中只是簡單介紹了一些,如果仔細研究下去還會發現很多有趣的點。
今天的文章到這裡就結束了,如果喜歡本文的話,請來一波素質三連,給我一點支援吧(關注、轉發、點贊)。
- END -