二分图匹配实际上属于网络流算法的应用
不过针对于二分图的特殊性,由网络流基本算法衍生出了更高效的算法
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最谦虚啊,不用自己的名字命名算法