引入
带花树解决的是一般图的最大匹配问题。
学习此算法前你需要掌握二分图匹配的匈牙利算法,至少需要知道它的思想。
理论
众所周知,二分图匹配的思想是不断地找增广路。
严谨地讲,是找到了一条简单路径,它的起点和终点都未匹配,并且路径上的边是 匹配边-非匹配边 相互交错。
但是在一般图中直接找增广路会出锅。
原因是二分图中可以保证增广的过程中匹配边都是从左边连到右边,但一般图中因为奇环的存在,使得这个方向是不定的。具体的原因不(lan)再(de)说明,自己画个图就可以看出。
我们注意到,二分图和一般图最本质,或者说唯一的区别就是是否存在奇环。
注:对于这个奇环虽然不一定简单,但直接把它当成简单环处理,后面会详细说明。
我们发现对于一个大小为2k+12k+12k+1的奇环,从任意一个点都可以向外匹配,并且环内剩下的2k2k2k个点可以组成kkk对匹配。
这么说的话,环上的每个点都是等效的。
这么说我们可以直接把这2k+12k+12k+1个点缩成一个点处理,我们把缩成的点称为"花"。
然后继续增广就可以了。
流程
前面说着简单其实全在口胡……本算法的难点全在实现上。
记录每个点的匹配点mim_imi,如果没有匹配mi=0m_i=0mi=0,显然初始时mi=0m_i=0mi=0
首先枚举所有结点,当发现一个未匹配点(即mi=0m_i=0mi=0)时,尝试从这个点为起点找一条增广路。
设这个点为sss,从sss为根开始做 BFS 建出一棵带花树。注意带花树是对一个根单独建的,也就是每次都要清空数据。
因为我们要找出增广路翻转,对每个结点iii记录一个前驱 preipre_iprei,表示如果以sss为增广路起点,iii为终点,路径上的倒数第二个点(也就是iii往上跳一个点)是哪个。
值得注意的是,找增广路的时候并不是一直跳preipre_iprei,因为增广路是交替的,跳一步之后下一步一定是匹配边。
所以跳一步preprepre后直接走到对应的匹配点,即不断地i←mpreii \leftarrow m_{pre_i}i←mprei,这在后面将很有用。
对点进行染色。一个点有三种状态:黑色,白色,未染色。开始时所有点都未染色。然后起点sss设为黑色。
每次从队列中取出一个点uuu,从后面的流程可以知道它一定是黑点。
访问所有与uuu相邻的点vvv,然后大力分类讨论。
- uuu和vvv在一个花中(也就是被缩成了一个点)
不知道咋处理,但脑电波一下应该没啥影响,所以跳过好了。
- vvv是白色
似乎还是没啥用,跳过好了。
- vvv未染色
如果 vvv没有被匹配,那么我们就找到了一条增广路,跳preprepre翻转所有边的匹配状态,答案+1+1+1,退出函数。
否则我们把vvv染成白色,并令prev=upre_v=uprev=u。因为vvv已经有匹配了,我们把mvm_vmv染成黑色并压进队列,表示从可以这里开始增广。
- vvv是黑色
最复杂的情况。当找到一个黑色的时候说明出现了一个奇环。
因为是 bfs,我们可以暴力跳preprepre找到以sss为根时uuu和vvv的 LCA ,记为ppp
我们需要把这条路径上的点合并。
可以用并查集维护,把路径上的点并查集的父亲设为ppp就可以了。注意花里面可能套了花,所以只考虑并查集的根。
然而还没完,因为你还要求出具体的匹配点,所以你需要维护环内的匹配情况。
图例:粗边为匹配边,细边为未匹配边,箭头为preprepre
为了贯彻落实“任何一个点都可以向外匹配”的性质,盯着这个图,发现任何一个黑点(即所有匹配边的下面那个)在外面有匹配边的时候,都可以向上整一条增广路出来。
以vvv为例:
我们想让白点(匹配边上面那个)也可以整一个出来,不难构造出
地 狱 绘 图
这样利用找增广路是隔一次跳一步的性质,白点会从下面绕一圈上去,完美地解决了这个问题。具体实现见代码。
然后在跳的过程中把白点染成黑点并入队,表示欢迎外面的点进来找增广路。
如果你不知道为什么原来就是黑色的点不入队,想想你把它染成黑色的时候在干什么。
总复杂度是O(n3)O(n^3)O(n3),实际上很松,可以当O(n2)O(n^2)O(n2)算
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cctype>
#include <queue>
#define MAXN 1005
#define MAXM 100005
using namespace std;
inline int read()
{int ans=0;char c=getchar();while (!isdigit(c)) c=getchar();while (isdigit(c)) ans=(ans<<3)+(ans<<1)+(c^48),c=getchar();return ans;
}
struct edge{int u,v;}e[MAXM];
int head[MAXN],nxt[MAXM],cnt;
void addnode(int u,int v)
{e[++cnt]=(edge){u,v};nxt[cnt]=head[u];head[u]=cnt;
}
int mat[MAXN],pre[MAXN],col[MAXN],fa[MAXN],n,m;
int idx[MAXN],tot;
inline int find(const int& x){return x==fa[x]? x:fa[x]=find(fa[x]);}
queue<int> q;
inline int lca(int x,int y)
{for (++tot;;swap(x,y))if (x){x=find(x);if (idx[x]==tot) return x;idx[x]=tot,x=pre[mat[x]]; }
}
inline void shrink(int x,int y,int l)
{while (find(x)!=l){pre[x]=y,y=mat[x];if (col[y]==2) col[y]=1,q.push(y);if (x==find(x)) fa[x]=l;if (y==find(y)) fa[y]=l;x=pre[y];}
}
inline int bfs(int s)
{while (!q.empty()) q.pop();q.push(s);col[s]=1;for (int i=1;i<=n;i++) pre[i]=col[i]=0,fa[i]=i;while (!q.empty()){int u=q.front();q.pop();for (int i=head[u];i;i=nxt[i]){int v=e[i].v;if (col[v]==2||find(u)==find(v)) continue;if (!col[v]){col[v]=2,pre[v]=u;if (!mat[v]){for (int last;v;v=last)last=mat[pre[v]],mat[v]=pre[v],mat[pre[v]]=v;return 1;}col[mat[v]]=1,q.push(mat[v]);}else{int l=lca(u,v);shrink(u,v,l),shrink(v,u,l);}}}return 0;
}
int main()
{n=read(),m=read();for (int i=1;i<=m;i++){int u,v;u=read(),v=read();addnode(u,v),addnode(v,u);}for (int i=1;i<=n;i++) fa[i]=i;int ans=0;for (int i=1;i<=n;i++)if (!mat[i])ans+=bfs(i);printf("%d\n",ans);for (int i=1;i<=n;i++) printf("%d%c",mat[i]," \n"[i==n]);return 0;
}