带花树算法学习小记

众所周知,带花树算法用来解决一般图最大匹配问题。

因为人很菜理解不清楚,所以建议对板理解。

以下都是本人的感性理解,有不严谨的说法请包涵。


参考资料:

https://blog.bill.moe/blossom-algorithm-notes/

https://www.cnblogs.com/cjyyb/p/8719368.html


同样是找增广路。增广路定义为一条路径(p_1,p_2,dots,p_k),满足(p_2,p_3)匹配,(p_4,p_5)匹配……(p_{k-2},p_{k-1})匹配。(p_1)(p_k)不在匹配中、

如果找到了增广路,把增广路上点调整一下匹配关系,就可以使匹配数增多。


(bel_x)表示(x)和哪个点匹配。还有(pre_x)表示(BFS)树上的前驱。

找增广路大概流程:BFS,遍历过的点要记录颜色。(S)不在匹配中,(S)为起点,一开始(S)为黑色。

设当前点为(u),枚举其一条边(v)

  1. 如果(v)没有被遍历过:(1)(v)不在匹配中,此时找到增广路。(2)(v)在匹配中,把(v)标记为白点,(bel_v)标记为黑点,把(bel_v)丢入队列。
  2. 如果(v)被遍历过:
    1. 如果(v)为白点,不管它。
    2. 如果(v)为黑点,进行开花操作。

算法核心:开花。

如果开花一定是找到了个奇环。定义此时(u,v)在BFS树上的LCA为花根,记为(rt)

考虑从花上的某个点出去,并且找到了增广路。那么一定可以从(rt)开始(其中(rt)是黑点),可以通过走花的一边,使得走的这一边都是白点-黑点交替。(这种说法并不准确,看着办吧)这条路径是头是黑点,尾是黑点,所以可以看成把花缩到花根上。后面找到增广路的时候就把花展开,展开的时候选一条花根到花中连出去的点的合法的路径。


实现是问题。

每个需要记录前驱。然而如果在花中可能前后两个点都是前驱,都要记下来。

为了方便实现,(pre_x)只是记(x)为白点意义下的前驱(因为在花中,(x)可能作为黑点,可能作为白点)。如果(x)为黑点,那么它往前两步是:(x o bel_x o pre_{bel_x})。这样就不需要另外记另一个前驱了。

求LCA时,轮流跳,跳到了之后打标记,直到跳到有标记的点为止。遍历次数是(2max(dis(u,rt),dis(v,rt))=O(花大小))(之前缩的花算一个点)。

开花:给花上的原来黑点的位置,计算它为白点意义下的前驱(pre_x),此时(pre_x)将指向不同于(bel_x)的方向。并且把这些白点都标记为黑点,丢入队列(此时标黑表示是否有丢入队列。)。(然而在实现的时候,开花操作是对没有缩点的图进行操作。)

用并查集来维护出每个点当前在哪个花中。

更多细节见代码注释。


怎么网上的好多板子说总时间是(O(nm*并查集时间)),为什么我感觉下面这个照着别人板子写了板子的时间复杂度是(O(n^3))

一次缩点会少至少两个点,所以会进行(O(n))次开花操作。然而这个板子里开花时间是(O(n))的。于是一次增广时间应该是(O(n^2))

理论上确实也可以做(O(m))吧,就是保证如果一个点被缩掉,开花的时候就不会遍历它。但不知道有什么简洁的实现方法。

如果有神仙路过希望指点。


using namespace std;
#include <bits/stdc++.h>
#define N 1005
#define M 200005
int n,m;
struct EDGE{
	int to;
	EDGE *las;
} e[M*2];
int ne;
EDGE *last[N];
void link(int u,int v){
	e[ne]={v,last[u]};
	last[u]=e+ne++;
}
queue<int> q;
int c[N];
int bel[N],pre[N],fa[N];
int getfa(int x){return fa[x]==x?x:fa[x]=getfa(fa[x]);}
int LCA(int x,int y){
	static int bz[N],BZ;
	++BZ,x=getfa(x),y=getfa(y);
	while (bz[x]!=BZ){
		bz[x]=BZ;
		x=getfa(pre[bel[x]]);
		if (y)//注意判断当点为0的时候不要跳
			swap(x,y);
	}
	return x;
}
void blossom(int x,int y,int w){
    //开花操作时一个一个跳,即使是被缩掉的点
	while (getfa(x)!=w){//一定要这么写
		pre[x]=y,y=bel[x];
		if (c[y]==0) c[y]=1,q.push(y);
		fa[x]=w;
		fa[y]=w;
        //注意不要像下面这么写,因为getfa(x)是x的祖先,到时候跳过去的时候会提前退出循环
//		fa[getfa(x)]=w;
//		fa[getfa(y)]=w;
		x=pre[y];
	}
}
int find(int S){
	while (!q.empty())
		q.pop();
	for (int i=1;i<=n;++i){
		c[i]=-1;
		pre[i]=0;
		fa[i]=i;
	}
	q.push(S);
	c[S]=1;
	while (!q.empty()){
		int x=q.front();
		q.pop();
		for (EDGE *ei=last[x];ei;ei=ei->las){
			int y=ei->to;
			if (getfa(x)==getfa(y))//别忘了
				continue;
			if (c[y]==-1){
				pre[y]=x;
				if (!bel[y]){
					for (int z=y,lst;z;z=lst)
						lst=bel[pre[z]],bel[z]=pre[z],bel[pre[z]]=z;
					return 1;
				}
				c[y]=0,c[bel[y]]=1;
				q.push(bel[y]);
			}
			else if (c[y]==1){
				int lca=LCA(x,y);
				blossom(x,y,lca);
				blossom(y,x,lca);
			}
		}
	}
	return 0;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%d%d",&n,&m);
	for (int i=1;i<=m;++i){
		int u,v;
		scanf("%d%d",&u,&v);
		link(u,v),link(v,u);
	}
	int ans=0;	
	for (int i=1;i<=n;++i)
		if (!bel[i]) 
			ans+=find(i);
	printf("%d
",ans);
	for (int i=1;i<=n;++i)
		printf("%d ",bel[i]);
	return 0;
}
原文地址:https://www.cnblogs.com/jz-597/p/14575213.html