Loading

学习笔记——双连通分量

前言

我们的神,MC 曾经曰过,Tarjan 是 \(11\) 级算法。

边双

桥: 在一张连通无向图中,如果去掉一条边使得图的极大连通分量增加了,那么这条边就叫做桥。
边双连通分量: 一张无向图中的一个极大连通子图不存在桥,那么这个子图就是边双(e-bcc)。

性质: 一张无向图进行边双缩点之后是一棵树。

求桥

直接记录 dfnlow,意义和 Tarjan 缩点相同。但是不用记录栈。遍历儿子的时候判断是否有 low[s]>dfn[x],如果是,说明儿子无法到达当前节点的祖先,也就是当前边是桥。对边记录编号后打标记即可。

void Tarjan(int x,int fa){
	dfn[x]=low[x]=++tot;
	for(pii s:g[x]){
		if(s.fi==fa) continue;
		if(!dfn[s.fi]){
			Tarjan(s.fi,x);
			low[x]=min(low[x],low[s.fi]);
			if(dfn[x]<low[s.fi]) vis[s.se]=vis[s.se^1]=1;
		}else low[x]=min(low[x],dfn[s.fi]);
	}
}

求 e-bcc

方法一: 用上面的方法求出所有的桥,然后去掉桥后每一个极大连通图就是一个 e-bcc。

void dfs(int x){
	col[x]=bcc;
	for(pii s:g[x]){
		if(vis[s.se]) continue;
		if(col[s.fi]) continue;
		dfs(s.fi);
	}
}

方法二: 也可以用 Tarjan 直接求 e-bcc。同样记录一个栈,在同一个 e-bcc 的点仍然会在一起。然后和普通的缩点没有很大区别。注意需要处理重边,因为两个点之间如果有重边,这两个点也属于同一个 e-bcc。我们直接在做的时候记录父亲,然后看儿子中父亲出现了几次,第一次出现的时候 continue,之后再出现说明是重边。

void Tarjan(int x,int fa){
	dfn[x]=low[x]=++tot;stk[++top]=x;
	bool flag=0;
	for(int s:g[x]){
		if(s==fa&&!flag){flag=1;continue;}
		if(!dfn[s]) Tarjan(s,x),low[x]=min(low[x],low[s]);
		else if(!col[s]) low[x]=min(low[x],dfn[s]);
	}if(dfn[x]==low[x]){
		bcc++;do{
			col[stk[top]]=bcc;
		}while(stk[top--]!=x);
	}
}

点双

割点: 在一张无向图中,去掉一个点和这个点所连接的边,极大连通分量增加,那么这个点是割点。
点双连通分量: 无向图中的一个极大连通子图,如果它没有割点,那么它就是一个点双连通分量(p-bcc)。

性质: 割点会同属于两个 p-bcc,所以在题目中我们一般采用圆方树来把点双问题转化为树上问题。

求割点

同样维护 dfnlow,如果 dfn[x]<=low[s],说明 \(s\) 不能跨越 \(x\) 到前面的点去,则 \(x\) 是一个割点。

需要注意,对于根节点而言,没有办法用这种方式判断它是否是割点,但是我们可以看它为根的 dfs 生成树上有几条相连的树枝边,如果有 \(2\) 条以上与它相连的树枝边,那么根就是一个割点。

void Tarjan(int x,int fa){
	dfn[x]=low[x]=++tot;
	int cnt=0;
	for(int s:e[x]){
		if(!dfn[s]){
			cnt++;Tarjan(s,x);
			low[x]=min(low[x],low[s]);
			if(x!=fa&&low[s]>=dfn[x])
				ans[x]=1;
		}else if(s!=fa)
			low[x]=min(low[x],dfn[s]);
	}if(x==fa&&cnt>=2)
		ans[x]=1;
}

求 p-bcc

我们注意到一个割点属于两个不同的 p-bcc,所以用删去点后求连通图的方法行不通,所以只能采用 Tarjan 来做。

其实也很简单,只要在求割点的时候,维护一个栈就行了。

void Tarjan(int x,int fa){
	dfn[x]=low[x]=++tot;
	stk[++top]=x;
	for(int s:e[x]){
		if(!dfn[s]){
			Tarjan(s,x);
			low[x]=min(low[x],low[s]);
			if(low[s]>=dfn[x]){
				cnt++;
				while(stk[top]!=s)
					bcc[cnt].push_back(stk[top--]);
				bcc[cnt].push_back(stk[top--]);
				bcc[cnt].push_back(x);
			}
		}else low[x]=min(low[x],dfn[s]);
	}
}

简单感性理解一下为什么不需要特判根。首先如果根是割点,那么肯定是最后一个点一个 p-bcc。否则它应当会在某一个儿子的时候,从那个割点开始弹栈,然后直到弹到根。

维护 vector 是方便建树和其它操作。

后记

我们的神,MC 曾经曰过 Tarjan,是 \(11\) 级算法。
真的是 \(11\) 级吗,确定不用更高一点?

posted @ 2022-11-13 10:02  ZCETHAN  阅读(79)  评论(3编辑  收藏  举报