【演算法學習筆記】26:匈牙利演算法(二分圖最大匹配)
1 簡述
給定一個二分圖,例如:
匈牙利演算法能夠快速的計算出一種匹配方式,使得匹配的數量最多。注意,一個成功的匹配方式中,沒有兩條邊是共用了同一個點的。
形象的說,這個問題可以理解成二分圖兩邊分別是男生和女生,有連線的表示可以湊成一對,匈牙利演算法就是用來計算最多能夠湊成多少對(不存在腳踏多條船的情況)。
例如左邊是男生,右邊是女生,可以任選一方為主動方,比如是男生方,那麼流程如下。
對於每個男生結點,對所有與之有連線的女生結點,檢查對應的女生是不是單身,如果是就直接湊成一對。那麼在上圖的例子中,前兩個男生都可以直接匹配到自己連線的第一個女生:
對於男生 3 3 3而言,他所能匹配的第一個女生是 2 2 2,但是這個女生已經是非單身了。這個時候就要去不斷嘗試,嘗試讓女生 2 2 2已經匹配的男生 1 1 1換一個匹配的女生(而不會讓當前已經成對的匹配數量減少)。
接下來男生
1
1
1檢查自己所能匹配的下一個女生
4
4
4,這個女生是非單身,所以將男
1
1
1與女
4
4
4匹配,此時女
2
2
2被釋放出來,得以和男
3
3
3匹配:
接下來檢查下一個男生
4
4
4,它所能匹配的女生
3
3
3是單身,將他倆匹配:
至此,能匹配的都匹配上了,這個二分圖的最大匹配數量是4。
2 模板題:二分圖的最大匹配
求最大匹配的時候,可以直接存到鄰接表裡,因為遍歷的時候是對每個男生遍歷所有能匹配的女生,所以只要存一下從男生到女生的邊,不需要像存普通無向圖那樣存雙向邊。
在實現時要注意,int match[]
陣列用來記錄每個女生當前匹配的男生是哪一個(存標號),如果單身裡面存的就是0
。
由於在匈牙利演算法中,即使一個女生已經有匹配了,也可能被更換匹配,所以還需要每次清空一個bool st[]
陣列,來記錄當前給某個男生找匹配的過程中,每個女生有沒有遍歷過,防止出現死迴圈。
#include <iostream>
#include <cstring>
using namespace std;
// 由於只存單項,邊M不用開兩倍
const int N = 510, M = 1e5 + 10;
// 鄰接表
int h[N], e[M], ne[M] , idx;
void add_edge(int a, int b) {
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++ ;
}
// match記錄女生當前匹配的男生
int match[N];
// st記錄當前這輪每個女生有沒有遍歷過
// st[j] = true時候,j這個女生是被禁用的
bool st[N];
// 匈牙利演算法,嘗試給x找一個女朋友
bool find(int x) {
// 對於能夠和x匹配的所有女生j
for (int i = h[x]; i != -1; i = ne[i]) {
int j = e[i];
// 如果j沒有被禁用(沒有遍歷過)
if (!st[j]) {
// 就將其鎖定,因為要嘗試讓x匹配j
st[j] = true;
// 如果j本來就是單身,或者j的男朋友能換一個女朋友
if (match[j] == 0 || find(match[j])) {
// 就將x匹配上j
match[j] = x;
// 因為x能找到女朋友,所以返回true
return true;
}
}
}
// 嘗試了x的所有能匹配的j都不行,就返回false
return false;
}
int main() {
// 讀取二分圖a->b
memset(h, -1, sizeof h);
int n1, n2, m;
cin >> n1 >> n2 >> m;
for (int i = 0; i < m; i ++ ) {
int a, b;
cin >> a >> b;
add_edge(a, b);
}
// 遍歷每個男生嘗試匹配
int res = 0;
for (int i = 1; i <= n1; i ++ ) {
memset(st, false, sizeof st);
// 每次嘗試都會嘗試讓i匹配進來
// 結果res是隻增不減的
if (find(i)) res ++ ;
}
cout << res << endl;
return 0;
}
特別要注意每輪要清空st
陣列,在這一輪中,嘗試讓女生j
匹配給男生x
時,就要設定st[j] = true
以將j
鎖定了,被鎖定的女生j
永遠不會解鎖。
匈牙利演算法基於貪心的思想,理論時間複雜度是多項式級別的,但是實際執行速度還是比較快的。