Loading

求强连通分量和双连通分量,但是不用 Tarjan

OI-wiki 真是个好东西。

我们在求强连通分量或双连通分量的时候,常常会用 Tarjan 算法来进行缩点。
但是我们也有不用 Tarjan 的连通分量算法。

1. Kosaraju 算法

如果我们要求的是有向图上的强连通分量,使用 Kosaraju 算法是一个不错的选择。
该算法的码量相比 Tarjan 来说会少不少。

Kosaraju 算法的流程如下:

  • 对原图做 dfs 进行遍历。对每个结点遍历完所有儿子之后再把它记录入栈。
  • 建出反图,按照栈的顺序对每个结点进行 dfs 染色。

直接看代码应该更好懂一些:

void dfs1(int u)
{
	vis[u]=1;
	for(int i=h[u];i;i=e[i].nxt)
	{
		int p=e[i].to;
		if(e[i].val==1&&!vis[p])dfs1(p);//val=1 代表原图,val=-1 代表反图
	}
	travel[++ncnt]=u;
}
void dfs2(int u)
{
	color[u]=scccnt;
	for(int i=h[u];i;i=e[i].nxt)
	{
		int p=e[i].to;
		if(e[i].val==-1&&!color[p])dfs2(p);
	}
}
//Kosaraju 主过程
for(int i=1;i<=n;i++)if(!vis[i])dfs1(i);
for(int i=ncnt;i>=1;i--)
	if(!color[travel[i]])
	{
		++scccnt;
		dfs2(travel[i]);
	}

看上去这个算法非常玄学。下面来说明它的大致思想和正确性。
我们知道在无向图上求连通分量只要直接 dfs 就完事了。
但是在有向图上,情况就没有这么简单。
考虑一条有向边 \(A\rarr B\),假设我们此时遍历到了 \(A\),并发现 \(B\) 还没有被遍历到,那么我们会经过这条边。
但是这样我们有可能就回不来了,这样 A 就不能算在强连通分量里面,此时我们就多算了 A 这个点,出现了错误。
于是我们的想法是,选定一个遍历顺序,使得对于每条边 \(A\rarr B\),要么先遍历到 \(B\),再遍历到 \(A\),要么能确定存在从 \(B\)\(A\) 的路径,这样我们就不会误入歧途。
Kosaraju 的方法是,反图上的强连通分量是和原图一样的。于是我们可以钦定一个顺序使得对每条不存在从 \(B\)\(A\) 的路径的边 \(A\rarr B\) 先遍历 \(A\) 再遍历 \(B\)(显然这比上面反过来判断的方法好做),然后在反图上 dfs。
要想到这种遍历顺序并不容易,但是我们可以来试着证明一下。
首先约定符号。记 \(dfn(u)\) 为我们一开始遍历出的顺序。这样我们第二次 dfs 的顺序就是这个顺序反过来(因为算法中我们使用栈进行记录)。于是我们只需要证明对原图上的边 \(A\rarr B\), 要么有 \(dfn(A)>dfn(B)\),要么能确定原图上存在另一条从 \(B\)\(A\) 的路径。
对不同的边进行分类讨论。

  • dfs 树边/前向边:显然。因为我们总是先记录儿子再记录父亲。
  • 后向边:这个时候后向边和树边组成了环,所以顺序随便选。
  • 横叉边/不同 dfs 树之间的边:因为这是 dfs 树,我们一定会先遍历到 \(B\) 再遍历到 \(A\)

证完了。

你也可以用相似的手段来说明其他的遍历方式不可行。比如对于直接的前序 dfs,虽然说能处理 dfs 树边、前向边和后向边,但是不能处理横叉边。

2. 边双连通分量

这里介绍的算法的好处是思想的简单。
先跑一遍dfs树,然后我们对非树边进行处理。
假设一条非树边连接了 \(u,v\) 两个结点,很明显这条树边和 \(u\)\(v\) 在树上的路径构成了一个环,成为边双连通分量。
于是我们只需要对每条非树边,将树上的对应链打上标记,最后在树上且未被打标记的边就是割边。
边双连通分量只要把割边删了 dfs 就行。
维护链标记的操作考虑树上差分。
注意无向图的 dfs 树没有横叉边,因此直接对深度大的点打上 1,对深度小的打上 -1 就完成了差分操作。
所以我们就轻松做到了线性。

代码可以看我在 luogu 模板题写的题解

3. 点双连通分量

我们知道每条边只能属于一个点双。于是我们考虑维护边构成的连通块。
具体地,每条非树边在树上对应的链属于同一个连通块,即属于同一个点双。于是要判断一个点是不是割点,只需看它连出的边是不是在一个点双内即可。

于是我们考虑怎么维护链组成的连通块。
根据上面提到的无向图 dfs 树的性质,我们只需要在每条边上维护一个标记,记录它和它向上的一条边是否属于一个连通块即可。
但是这次标记就不好维护了, 因为标记打在链上, 我们需要先把链上的标记转到深度大的点上. 但这样操作后, 你会发现深度小记的点的位置并不好找, 需要用深度大的点做一个倍增才行, 复杂度就多了一个 \(\log\).

(暂时不会线性做法)

posted @ 2022-08-03 19:27  pjykk  阅读(36)  评论(0编辑  收藏  举报