带花树学习笔记

引入

带花树解决的是一般图的最大匹配问题。

学习此算法前你需要掌握二分图匹配的匈牙利算法,至少需要知道它的思想。

理论

众所周知,二分图匹配的思想是不断地找增广路。

严谨地讲,是找到了一条简单路径,它的起点和终点都未匹配,并且路径上的边是 匹配边-非匹配边 相互交错。

但是在一般图中直接找增广路会出锅。

原因是二分图中可以保证增广的过程中匹配边都是从左边连到右边,但一般图中因为奇环的存在,使得这个方向是不定的。具体的原因不(lan)再(de)说明,自己画个图就可以看出。

我们注意到,二分图和一般图最本质,或者说唯一的区别就是是否存在奇环。

注:对于这个奇环虽然不一定简单,但直接把它当成简单环处理,后面会详细说明。

我们发现对于一个大小为 2 k + 1 2k+1 2k+1的奇环,从任意一个点都可以向外匹配,并且环内剩下的 2 k 2k 2k个点可以组成 k k k对匹配。

这么说的话,环上的每个点都是等效的。

这么说我们可以直接把这 2 k + 1 2k+1 2k+1个点缩成一个点处理,我们把缩成的点称为"花"。

然后继续增广就可以了。

流程

前面说着简单其实全在口胡……本算法的难点全在实现上。

记录每个点的匹配点 m i m_i mi,如果没有匹配 m i = 0 m_i=0 mi=0,显然初始时 m i = 0 m_i=0 mi=0

首先枚举所有结点,当发现一个未匹配点(即 m i = 0 m_i=0 mi=0)时,尝试从这个点为起点找一条增广路。

设这个点为 s s s,从 s s s为根开始做 BFS 建出一棵带花树。注意带花树是对一个根单独建的,也就是每次都要清空数据。

因为我们要找出增广路翻转,对每个结点 i i i记录一个前驱 p r e i pre_i prei,表示如果以 s s s为增广路起点, i i i为终点,路径上的倒数第二个点(也就是 i i i往上跳一个点)是哪个。

值得注意的是,找增广路的时候并不是一直跳 p r e i pre_i prei,因为增广路是交替的,跳一步之后下一步一定是匹配边。

所以跳一步 p r e pre pre后直接走到对应的匹配点,即不断地 i ← m p r e i i \leftarrow m_{pre_i} imprei,这在后面将很有用。

对点进行染色。一个点有三种状态:黑色,白色,未染色。开始时所有点都未染色。然后起点 s s s设为黑色。

每次从队列中取出一个点 u u u,从后面的流程可以知道它一定是黑点。

访问所有与 u u u相邻的点 v v v,然后大力分类讨论。

  1. u u u v v v在一个花中(也就是被缩成了一个点)

不知道咋处理,但脑电波一下应该没啥影响,所以跳过好了。

  1. v v v是白色

似乎还是没啥用,跳过好了。

  1. v v v未染色

如果 v v v没有被匹配,那么我们就找到了一条增广路,跳 p r e pre pre翻转所有边的匹配状态,答案 + 1 +1 +1,退出函数。

否则我们把 v v v染成白色,并令 p r e v = u pre_v=u prev=u。因为 v v v已经有匹配了,我们把 m v m_v mv染成黑色并压进队列,表示从可以这里开始增广。

  1. v v v是黑色

最复杂的情况。当找到一个黑色的时候说明出现了一个奇环。

因为是 bfs,我们可以暴力跳 p r e pre pre找到以 s s s为根时 u u u v v v的 LCA ,记为 p p p

我们需要把这条路径上的点合并。

可以用并查集维护,把路径上的点并查集的父亲设为 p p p就可以了。注意花里面可能套了花,所以只考虑并查集的根。

然而还没完,因为你还要求出具体的匹配点,所以你需要维护环内的匹配情况。

在这里插入图片描述

图例:粗边为匹配边,细边为未匹配边,箭头为 p r e pre pre

为了贯彻落实“任何一个点都可以向外匹配”的性质,盯着这个图,发现任何一个黑点(即所有匹配边的下面那个)在外面有匹配边的时候,都可以向上整一条增广路出来。

v v v为例:

在这里插入图片描述我们想让白点(匹配边上面那个)也可以整一个出来,不难构造出

在这里插入图片描述

地 狱 绘 图

这样利用找增广路是隔一次跳一步的性质,白点会从下面绕一圈上去,完美地解决了这个问题。具体实现见代码。

然后在跳的过程中把白点染成黑点并入队,表示欢迎外面的点进来找增广路。

如果你不知道为什么原来就是黑色的点不入队,想想你把它染成黑色的时候在干什么。

总复杂度是 O ( n 3 ) O(n^3) O(n3),实际上很松,可以当 O ( n 2 ) 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;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值