二分圖匹配實際上屬於網絡流算法的應用
不過針對於二分圖的特殊性,由網絡流基本算法衍生出了更高效的算法
1、二分圖最大匹配
模板題:https://www.luogu.org/problemnew/show/P3386
求二分圖的最大匹配,可以將其轉化為求最大流
只要將S向X集合所有點連一條邊,再從Y集合每個點向T連一條邊,所有邊的邊權為1,求S到T的最大流即可
不過很明顯,這樣的算法沒有利用二分圖匹配問題的特殊性:
1、所有邊的邊權為1
2、一共只有兩大組點
於是,我們可以找到一種更高效,更簡便的算法。此時便引入了交替路的概念。
交替路是這樣的一條路徑:第一條邊和最后一條邊均不屬於當前匹配,而中間的邊一條屬於一條不屬於,交替分布。
那么此時,我們可以將這條路徑上的邊做一個類似於“異或”的操作:
讓原來屬於匹配的邊現在不屬於,而原來不屬於匹配的邊現在屬於。於是匹配中便多了一條邊。
不斷尋找交替路,就能每次至少向匹配中增加至少一條邊。
代碼如下:
#include <bits/stdc++.h> using namespace std; int n,m,e; vector<int> a[2005]; int match[2005]; bool vis[2005]; void add_edge(int u,int v) { a[v].push_back(u); a[u].push_back(v); } bool dfs(int v) { for(int i=0;i<a[v].size();i++) { int u=a[v][i],m=match[u]; if(!vis[u]) { vis[u]=true; if(m==-1 || dfs(m)) { match[u]=v; match[v]=u; return true; } } } return false; } int main() { cin >> n >> m >> e; for(int i=1;i<=e;i++) { int x,y;cin >> x >> y; if(y>m) continue; add_edge(x,n+y); } int res=0; memset(match,-1,sizeof(match)); for(int i=1;i<=n;i++) { memset(vis,0,sizeof(vis)); if(match[i]==-1) res+=dfs(i); } cout << res << endl; return 0; }
代碼比較簡潔,其中有幾個地方是可以變動的:
1、對於$match$數組,可以選擇同時記錄$X$和$Y$集合中的點,但也可以只記錄$Y$集合中的點。不過不能只記錄$X$集合中的點
2、對於$vis$數組,有兩種定義方式:
(1)用$vis$數組存放$X$集合中的點是否已走過,此時就要在剛剛進入函數時更新$vis[v]=true$,而在判斷時由
if(match[u]==-1 || dfs(match[u]))
改為
if(match[u]==-1 || !vis[match[u]] && dfs(match[u]))
(2)也可以像模板中那樣有$vis$存放$Y$集合中的點是否走過,而此時要在遞歸前賦值$vis[i]=true$
3、在進入遞歸前的判斷
if(match[i]==-1) res+=dfs(i);
可以省去,因為每次$dfs$時不會找到i之后的點
2、二分圖最優匹配
模板題:https://vjudge.net/problem/HDU-2255
對於保證有完全匹配的二分圖求最優匹配,便要用到$KM$算法
這個算法結合了最小費用流中貪心的算法和匈牙利算法中交替路增廣的思想,同時又有一項獨到的創新:引入頂標
對於$X$和$Y$集合中的每個點都添加一個頂標$lx[u]$和$ly[v]$,保證在任一時刻對於每條邊都有$lx[u]+ly[v]>=weight(u,v)$。
這樣在匹配完成時,假設處於匹配中的邊的總和為$P$,一定有$\sum_{i=1}^n lx[i]+ly[i]>=P$
而當$\sum_{i=1}^n lx[i]+ly[i]=P$時,$P$達到了下確界,從而此時的$P$就是最優匹配時的最大值
而要使得$\sum_{i=1}^n lx[i]+ly[i]=P$,則要使得匹配中的每一條邊都滿足$lx[u]+ly[v]=weight(u,v)$
這樣的邊稱作可行邊,而全部由可行邊組成的子圖稱作相等子圖
那么我們只要不斷貪心地選取可行邊、構造可行邊即可
上述用到了最小費用流貪心的方法以及匈牙利算法中增廣的方法
那么如何構造可行邊呢?
找到所有u屬於交替路,而v不屬於交替路的邊,
設$delta=min(lx[u]+ly[v]−weight(u,v))$,$lx[u]-=delta$,$ly[v]+=delta$。
這樣做就恰好使得一條邊$edge(u,v)$的$lx[u]+ly[v]=weight(u,v)$,相當於恰好增加了一條可行邊,而對於所有其他邊都不會有影響
(易證,記住$u$屬於交替路,而$v$不屬於交替路)
初始化:$lx[i]=max(edge(i,j))$,$ly[i]=0$
代碼如下:
#include <cstdio> #include <cstdlib> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int MAXN=305; const int INF=1<<27; int n,lx[MAXN],ly[MAXN],G[MAXN][MAXN],match[MAXN]; bool visx[MAXN],visy[MAXN]; int read() { char ch;int num,f=0; while(!isdigit(ch=getchar())) f|=(ch=='-'); num=ch-'0'; while(isdigit(ch=getchar())) num=num*10+ch-'0'; return f?-num:num; } bool dfs(int t) { visx[t]=true; for(int i=1;i<=n;i++) if(!visy[i] && lx[t]+ly[i]==G[t][i]) //一定是可行邊才可以繼續dfs,貪心思想 { visy[i]=true; if(match[i]==-1 || dfs(match[i])) { match[i]=t; return true; } } return false; } int main() { while(cin >> n) { memset(lx,0,sizeof(lx));memset(ly,0,sizeof(ly)); memset(match,-1,sizeof(match)); for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) G[i][j]=read(),lx[i]=max(lx[i],G[i][j]); } for(int k=1;k<=n;k++) while(true) { memset(visx,0,sizeof(visx)); memset(visy,0,sizeof(visy)); if(dfs(k)) break; else { int d=INF; for(int i=1;i<=n;i++) if(visx[i]) for(int j=1;j<=n;j++) if(!visy[j]) d=min(d,lx[i]+ly[j]-G[i][j]); //對delta更新 for(int i=1;i<=n;i++) { if(visx[i]) lx[i]-=d; if(visy[i]) ly[i]+=d; } } } int res=0; for(int i=1;i<=n;i++) res+=(lx[i]+ly[i]); cout << res << endl; } return 0; }
Tips:
1、$visx$和$vixy$每次$dfs$前都要初始化
2、只有找不到交替路才增邊,否則直接考慮下一個點
這樣的算法復雜度為$O(N^4)$,通過一個松弛數組$slack$的優化可使復雜度降到$O(N^3)$
其核心思想其實就是在$dfs$的過程中順帶更新對於$Y$集合中每個元素的相對於$X$集合的$delta$的值
代碼如下:
#include <cstdio> #include <cstdlib> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int MAXN=305; const int INF=1<<27; int n,lx[MAXN],ly[MAXN],G[MAXN][MAXN],match[MAXN],slack[MAXN]; bool visx[MAXN],visy[MAXN]; int read() { char ch;int num,f=0; while(!isdigit(ch=getchar())) f|=(ch=='-'); num=ch-'0'; while(isdigit(ch=getchar())) num=num*10+ch-'0'; return f?-num:num; } bool dfs(int t) { visx[t]=true; for(int i=1;i<=n;i++) { if(visy[i]) continue; int d=lx[t]+ly[i]-G[t][i]; if(!d) { visy[i]=true; if(match[i]==-1 || dfs(match[i])) { match[i]=t; return true; } } else slack[i]=min(slack[i],d); //如果不是可行邊,則將slack更新 } return false; } int main() { while(cin >> n) { memset(match,-1,sizeof(match)); memset(lx,0,sizeof(lx));memset(ly,0,sizeof(ly)); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) G[i][j]=read(),lx[i]=max(lx[i],G[i][j]); for(int k=1;k<=n;k++) { fill(slack,slack+300+1,INF); while(true) { memset(visx,false,sizeof(visx)); memset(visy,false,sizeof(visy)); if(dfs(k)) break; else { int d=INF; for(int i=1;i<=n;i++) if(!visy[i]) d=min(d,slack[i]); //只要Y集合中第i個元素不在交錯路中,就通過slack對d松弛 for(int i=1;i<=n;i++) { if(visx[i]) lx[i]-=d; if(visy[i]) ly[i]+=d; else slack[i]-=d; //由於lx[i]-=d,那么slack數組同樣要-=d。 } } } } int res=0; for(int i=1;i<=n;i++) res+=G[match[i]][i]; cout << res << endl; } return 0; }
此處的優化實際上類似於預處理優化的思想,將松弛的復雜度由$O(N^2)$降到了$O(N)$
那么對於一下代碼為什么只要$!visy[v]$就夠了,而不需要對$visx[u]$判斷呢
if(!visy[i]) d=min(d,slack[i]);
if(visy[i]) ly[i]+=d; else slack[i]-=d;
分一下幾種情況討論:
1、存在$edge(u,i)$,其中$visx[u]=true$:√
2、所有的$edge(u,i)$中$visx[u]=false$,且$i$還未遍歷過:$slack[i]=INF$,無關緊要 √
3、所有的$edge(u,i)$中$visx[u]=false$,但$i$已經遍歷過,這種情況不存在
(因為如果i存在交替路中,則一定有$visx[u]=true$的$u$與其相連)
學完后感嘆於這兩種算法引入的一些新概念:交替路、頂標
而且還真是Edmonds最謙虛啊,不用自己的名字命名算法
