【learning】一般图最大匹配——带花树
问题描述
对于一个图\(G(V,E)\),当点对集\(S\)满足任意\((u,v)\in S\),均有\(u,v\in V,(u,v)\in E\),且\(S\)中没有点重复出现,我们称\(S\)为\(G\)的一个匹配,当且仅当\(|S|\)最大时,称\(S\)为\(G\)的最大匹配
那么要如何求解一个图的最大匹配呢?
特殊图上?
首先考虑特殊图的最大匹配问题,也就是很经典的二分图最大匹配,这个问题可以用匈牙利算法解决,这里就不再赘述具体的实现等细节问题,我们只回顾一下这个算法的核心思想
我们从一个未匹配点出发,寻找一个可行的匹配点。其中最为核心的思想就是寻找一条交错轨(起始边和结束边都为未匹配边,中间匹配边和为匹配边交替出现,且整条路径的长度为奇数)然后对整条路径进行增广
那么现在思考一个问题,是否可以将这个思想运用到一般图上呢?
答案显然是不行的。
我们分析一下,会得到一个结论,交错轨增广合法有效的前提是,图中不存在奇环(环中包含的点数为奇数的环),否则的话可能会增广出一个点同时是两个点的匹配点的情况,这显然是不合法的
而偶环显然不会存在这种情况(因为点数是偶数),这也说明了为什么交错轨增广只能在二分图中使用,因为二分图中不存在奇环。
那么现在问题就集中在奇环上面了,这也是带花树要解决的主要问题
花
既然奇环这个东西这么麻烦,那就干脆拎出来处理。给它一个比较好听的名字叫花
比如说上面蓝色的三个点组成的奇环在进行了缩点操作(之后会解释)之后可以被称作花,一朵花代表的奇环中还可以有花
考虑一下奇环的性质,我们可以得到这样的几个结论:
1.对于包含\(k\)个节点的奇环,最大匹配数为\(\frac{k-1}{2}\),最大匹配点数为\(k-1\)
2.没有被匹配到的点可以在奇环中的任意一个位置,不会影响结果
显然,因为我们要求的是最大匹配,每个暂时没有被匹配上的点我们都要在答案不会变坏的前提下尽可能的给它寻找一个匹配点,而由于奇环具有上面的两个性质,所以我们在遇到一个奇环的时候可以只考虑其中没有匹配上的那个点(因为一旦这个点确定了,其他的点的匹配情况也可以直接还原出来,方案唯一)
所以,我们可以将一个奇环缩成一个点(花)来考虑,且这样处理与直接考虑原图的最大匹配等价
那么这样我们就将奇环的问题完美处理了,“开花”之后的图中不会存在奇环,这样一来我们就可以沿用交错轨增广的思想了
具体实现
整个匹配的过程可以分为\(n\)个阶段(\(n\)为点数),我们从每个未匹配的点出发,bfs寻找一条增广路径。为了方便匹配,我们将点分为\(A\)类和\(B\)类两类,边走边将遍历到的点标号,需要注意的是,在匹配的每个阶段我们都需要重新给点标号(否则就达不到增广的目的了),且我们只考虑从\(A\)类点出发进行增广(可以类比二分图匹配)
因为中间的增广涉及到沿路上的匹配边反转,我们还需要一个数组\(rec\)来记录来的方向,也就是这个点是从哪个点走过来的
开始寻找增广路径之前,我们将起始点设为\(A\)类点,那么在增广的过程中可能遇到以下几种情况
找到了一个未匹配点
说明我们找到了一条增广路径,直接使用\(rec\)数组和\(match\)数组把过来的路走一遍然后反转一下就好,然后返回\(true\),说明找到了一个可行的匹配点
if (!match[u]){
rec[u]=v;
for (int x=u,nxt,mnxt;x;x=mnxt){
nxt=rec[x]; mnxt=match[nxt];
match[nxt]=x;
match[x]=nxt;
}
return true;
找到了一个未被分类但已经匹配了的点
我们将这个点标记为\(B\)类,将其匹配点标记为\(A\)类,然后将匹配点丢到队列里面去等待增广
找到了一个\(B\)类点
说明找到了一个偶环,而根据上面的分析,偶环是不会影响算法的正确性的,直接忽略就好了
找到了一个A类点
说明找到了一个奇环,那么我们需要做的事情是把这个奇环缩成一朵花,具体的操作就是寻找这两个点路径上面的”\(lca\)“,然后将这两个点分别往\(lca\)缩(这里我们用并查集来实现),画个图直观理解一下是这样:
找\(lca\)写起来就是这样的,暴力往前跳,然后因为花中也可能有花,所以要用并查集的get_f获得代表点
int get_lca(int x,int y){
++T;
while (1){
if (x){
x=get_f(x);
if (vis[x]==T) return x;
vis[x]=T;
x=rec[match[x]];
}
swap(x,y);
}
}
合并也是十分简单粗暴,调用两次,直接沿着路径把两个点分别往\(lca\)缩就好了,由于奇环中所有的点都可能作为没有被匹配到的那个,所以都要标成\(A\)类然后丢到队列里面等待增广
void merge(int x,int y){
if (get_f(x)!=get_f(y)) f[x]=y;
}
void shrink(int x,int p){
int mx,nxt;
while (x!=p){
mx=match[x]; nxt=rec[mx];
if (get_f(nxt)!=p) rec[nxt]=mx;
if (tp[mx]==2)
tp[mx]=1,q.push(mx);
if (tp[nxt]==2)
tp[nxt]=1,q.push(nxt);
merge(x,mx);
merge(mx,nxt);
x=nxt;
}
}
找到了一个被缩在同一朵花里的点
这个就很简单啦直接忽略就好
至此,所有情况都已经被考虑完啦,由于整个算法分为\(n\)个阶段,每个阶段最多遍历整个图一次,每个点最多被缩成花\(n\)次,所以总体的复杂度是\(O(n^3)\)
\(UOJ79\)模板题,这里把完整的代码贴一下
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int N=510,M=124750;
struct xxx{
int y,next;
}a[M*2];
queue<int> q;
int h[N],rec[N],match[N],f[N],tp[N];
int vis[N];
int n,m,tot,ans,T;
void add(int x,int y);
int get_f(int x){return f[x]=f[x]==x?f[x]:get_f(f[x]);}
bool agu(int st);
void init(int st);
int get_lca(int x,int y);
void shrink(int x,int p);
void solve();
int main(){
#ifndef ONLINE_JUDGE
freopen("a.in","r",stdin);
#endif
int x,y;
scanf("%d%d",&n,&m);
memset(h,-1,sizeof(h));
tot=0;
for (int i=1;i<=m;++i){
scanf("%d%d",&x,&y);
add(x,y); add(y,x);
}
solve();
}
void add(int x,int y){
a[++tot].y=y; a[tot].next=h[x]; h[x]=tot;
}
bool agu(int st){
init(st);
int u,v,lca;
while (!q.empty()){
v=q.front(); q.pop();
for (int i=h[v];i!=-1;i=a[i].next){
u=a[i].y;
if (match[u]==v) continue;
if (get_f(u)==get_f(v)) continue;
if (tp[u]==2) continue;
if (tp[u]==1){//奇环
lca=get_lca(u,v);
if (get_f(u)!=lca) rec[u]=v;
if (get_f(v)!=lca) rec[v]=u;
shrink(v,lca);
shrink(u,lca);
}
else if (!match[u]){
rec[u]=v;
for (int x=u,nxt,mnxt;x;x=mnxt){
nxt=rec[x]; mnxt=match[nxt];
match[x]=nxt;
match[nxt]=x;
}
return true;
}
else{
rec[u]=v;
tp[u]=2;
tp[match[u]]=1;
q.push(match[u]);
}
if (tp[u]) continue;
}
}
return false;
}
void init(int st){
while (!q.empty()) q.pop();
for (int i=1;i<=n;++i)
f[i]=i,vis[i]=false,tp[i]=0,rec[i]=0;
q.push(st);
tp[st]=1;
}
int get_lca(int x,int y){
++T;
while (1){
if (x){
x=get_f(x);
if (vis[x]==T) return x;
vis[x]=T;
x=rec[match[x]];
}
swap(x,y);
}
}
void merge(int x,int y){
if (get_f(x)!=get_f(y)) f[x]=y;
}
void shrink(int x,int p){
int mx,nxt;
while (x!=p){
mx=match[x]; nxt=rec[mx];
if (get_f(nxt)!=p) rec[nxt]=mx;
if (tp[mx]==2)
tp[mx]=1,q.push(mx);
if (tp[nxt]==2)
tp[nxt]=1,q.push(nxt);
merge(x,mx);
merge(mx,nxt);
x=nxt;
}
}
void solve(){
for (int i=1;i<=n;++i)
if (!match[i]) agu(i);
for (int i=1;i<=n;++i)
if (match[i]>i) ++ans;
printf("%d\n",ans);
for (int i=1;i<=n;++i)
printf("%d ",match[i]);
}