二分圖的最大匹配:匈牙利算法
講之前本蒟蒻先普及一個重要專業名詞
增廣路。
如果你仔細讀過並畫過圖,不難發現如果找到一條增廣路,那么配對的個數就會加1。
所以說,增廣路的本質其實就是一條路徑的起點和終點都未配對的點的邊。
匈牙利算法:
這個叫匈牙利算法(Hungarian method)的東西是由匈牙利數學家Edmonds於1965年提出,所以叫匈牙利算法。匈牙利算法是二分圖匹配最常見的算法,該算法的核心就是尋找增廣路徑,它是一種用增廣路徑求二分圖最大匹配的算法。
復雜度:
時間復雜度 :
鄰接矩陣最壞為 $ O( n^3 ) $
鄰接表:$ O(mn) $
空間復雜度 :
鄰接矩陣:$ O( n^2 ) $
鄰接表:$ O(n+m) $
另一個重要概念:二分圖
二分圖是圖論中的一種特殊模型。 設G=(V,E)是一個無向圖,如果頂點V可分割為兩個互不相交的子集(A,B),並且圖中的每條邊(i,j)所關聯的兩個頂點i和j分別屬於這兩個不同的頂點集,則稱圖G為一個二分圖。
學過高中數學的話應該能看懂我在說什么(逃
簡而言之,就是頂點集V可分割為兩個互不相交的子集,並且圖中每條邊依附的兩個頂點都分屬於這兩個互不相交的子集,兩個子集內的頂點不相鄰。滿足這樣的圖就叫二分圖。
但我們怎么判斷一個圖是不是二分圖???
其實也不難,用紅藍點的方法就行。首先講任意的一個頂點染成紅色,再把這個點相鄰的頂點染成藍色,如果按照這種染色方式可以將所有的頂點全部着色,並且相鄰的頂點的顏色不同,那么該圖就是一個二分圖。
這里貼一下代碼
#define MAXV 1000//這里應該根據題目自定
vector<int> G[MAXV]; //圖
int V; //頂點數
int color[MAXV]; //頂點的顏色 (1 or -1)
//頂點v,顏色c
bool dfs(int v,int c){
color[v] = c;
//把當前頂點相鄰的頂點掃一遍
for(int i = 0;i < G[v].size(); i++){
//如果相鄰頂點已經被染成同色了,說明不是二分圖
if(color[G[v][i]] == c) return false;
//如果相鄰頂點沒有被染色,染成-c,看相鄰頂點是否滿足要求
if(color[G[v][i]] == 0 && !dfs(G[v][i],-c)) return false;
}
//如果都沒問題,說明當前頂點能訪問到的頂點可以形成二分圖
return true;
}
void solve(){
//可能是不連通圖,所以每個頂點都要dfs一次
for(int i = 0;i < V; i++){
if(color[i] == 0){
//第一個點顏色為 1
if(!dfs(i,1)){
cout << "No" << endl;
return;
}
}
}
}
這才是正文!!!
既然上面本蒟蒻已經介紹完了有關二分圖的知識,那下面該講下匈牙利算法了!!!
根據上文的描述,既然增廣路的作用是“改進匹配方案”(即增加配對數),那么如果我們已經找到了一種匹配方案,不難發現如果在當前匹配方案下再也找不到任何增廣路的話,那么當前匹配就是二分圖的最大匹配,算法如下。
1.首先從任意的一個未配對的點u開始,從點u的邊中任意選一條邊(假設這條邊是從 $ u->v $ )開始配對。如果點v未配對,則配對成功,這是便找到了一條增廣路。如果點v已經被配對,就去嘗試“連鎖反應”,如果這時嘗試成功,就更新原來的配對關系。
所以這里要用一個 $ matched[v] = u $ 。配對成功就將配對數加1,。
2.如果剛才所選的邊配對失敗,那就要從點u的邊中重新選一條邊重新去試。直到點u
配對成功,或嘗試過點u的所有邊為止。
3.接下來就繼續對剩下的未配對過的點一一進行配對,直到所有的點都已經嘗試完畢,找不到新的增廣路為止。
4.輸出配對數。
CODE:
//如果你已經讀完題,請自動從int main()處開始閱讀
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#define N 2010
using namespace std;
int n,m,e,ans;
int vis[N][N];
int ask[N],matched[N];
inline bool found(int x){ //dfs找增廣路
for (int i = 1 ; i <= m ; i++)
if (vis[x][i]){
if (ask[i])
continue;
ask[i] = 1;
if (!matched[i] || found(matched[i])) {
matched[i] = x ;
return true;
}
}
return false;
}
inline void match(){
int cnt = 0;//cnt是計數器
memset(matched,0,sizeof(matched));
for (int i = 1 ; i <= n ; i++){
memset(ask,0,sizeof(ask));
if (found(i))
cnt++; //找到了就加1
}
ans = cnt;
}
//從這里向下看起
int main(){
scanf("%d%d%d",&n,&m,&e);//結點個數分別為n,m,邊數為e
for (int i = 1 ; i <= e ; i++){
int x,y;
scanf("%d%d",&x,&y);
vis[x][y] = 1;
}
match();///匈牙利算法,見上
printf("%d \n",ans);
return 0;
}
再補一個臨接表的實現(就只放函數)
bool dfs(int x) {
for(int i = head[x] ; i ; i = e[i].to) {
int v = e[i].from;
if(!used[v]) {
used[v] = 1;
if(!matched[v] || dfs(matched[i]) ) {
matched[v] = u;
return 1;
}
}
}
return 0;
}
int match() {
int res = 0;
memset(match,-1,sizeof(match) );
for(int u = 0 ; u < uN ; u++) {
memset(used, -1 , sizeof(used));
if(dfs(u)) res++;
}
return res;
}
完結撒花(逃!!!