浅谈点双连通分量

本文章同步发表于本人 \(\texttt{luogu}\) 博客。

文章就点双连通分量进行讲解。

评测地址:Luogu P8435 【模板】点双连通分量

什么是割点

在一个无向联通图 \(G = (V, E)\) 中,若对于 \(x \in V\) ,从图中删去节点 \(x\) 及所有与 \(x\) 直接关联的边后, \(G\) 分裂成两个或两个以上不连通的子图,则称 \(x\) 为图 \(G\) 的割点。

换言之,割点就是指在图中删掉该点后,图变得不连通的点。

如下图,图中的点一就是一个割点。

什么是点双连通分量

点双联通分量是指在图 \(G\) 不含割点的极大子图。

在上面的图中,点 \(1, 2, 3\) 和点 \(1, 4, 5\) 构成的子图是原图中的两个点双联通分量。

注:一定要记住点双联通分量是极大的!!(不要像我一样认为图中任意两点和他们的连边都是一个点双)。

怎样求点双连通分量

如果没有学过有向图强连通分量的请右转 缩点

求点双联通分量的步骤如下:

  • 找到一个割点。(步骤一)

  • 割点和割点下面的还在栈里的就是一个点双联通分量。(步骤二)

下面我们对他们进行一个简单的感性理解(主要是不会严谨证明)


在 tarjan 算法中,我们使用了 \(dfn\)\(low\) 数组,分别表示一个点的时间戳和这个点往下能搜索到的时间戳最小的点。

  • 对于割点的证明(步骤一)

在无向图的双联通分量里,对于两个点 \(x, y\)\(y\) 表示与 \(x\) 直接相连的一个点)。如果 \(low_y \ge dfn_x\),就说明 \(y\) 无论怎样走也没办法走回到 \(x\) 了。(大家可以画一个搜索树来理解,这里不再赘述)。这样,点 \(y\) 和它的子树就相当于是一个封闭的系统了。这样,如果把点 \(x\) 删掉,下面的 \(y\) 和它的子树就孤立出来了,整个图变得不连通,因此 \(x\) 就是这个图的一个割点。代码如下:

if (low[j] >= dfn[u])

(好吧只有一行)

  • 对于割点下即为点双连通分量的证明(步骤二)

反证法。

假设割点 \(x\) 下面的不是点双连通分量,则割点 \(x\) 下面的子树里,一定有一个割点 \(y\)(由点双连通分量的性质易得)。

易得 \(y\) 会比 \(x\) 后遍历到。那么在弹栈时,我们会先将点 \(y\) 从栈里弹出来。

因此点 \(x\) 下面没有割点。与假设矛盾。

证毕。

步骤二代码如下:

if (low[j] >= dfn[u]) {
   			cnt ++ ;
   			int y;
   			do {
   				y = stk[top -- ];
   				res[cnt].push_back(y);
   			} while (y != j);
   			res[cnt].push_back(u);
   		}

另外,还需要注意一下下面这些地方:

  1. 一个割点可能属于许多个点双连通分量。因此弹栈时不能直接弹出割点。

  2. 可能存在孤立点。孤立点也属于点双连通分量,需要特判。

  3. 数据存在自环(好恶心

完整代码如下:

// By --- Lcy
// Date --- 2022.07.14

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 500010, M = 4000010;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int n, m, stk[N], top, cnt;
vector<int> res[N];

void add(int a, int b)
{
	e[ ++ idx] = b, ne[idx] = h[a], h[a] = idx;
}

void tarjan(int u, int root)
{
	dfn[u] = low[u] = ++ timestamp;
	stk[ ++ top] = u;
	
	if (u == root && !h[u]) {res[ ++ cnt].push_back(u); return; } // 对孤立点的特判
	// 如果为搜索树的顶端且没有出边,则为一个孤立点 
	
	for (int i = h[u]; i; i = ne[i])
	{
		int j = e[i];
		if (!dfn[j]) {
			tarjan(j, root);
			low[u] = min(low[u], low[j]);
			if (low[j] >= dfn[u]) { // 判断是否为割点 
				cnt ++ ; // 割点下面必定有一个点双连通分量 
				int y;
				do {
					y = stk[top -- ];
					res[cnt].push_back(y);
				} while (y != j); // 注意是 y != j 而不是 x != j,因为我们不能把割点弹出来 
				res[cnt].push_back(u); // 最后别忘了把割点弹出来 
			}
		}
		else
			low[u] = min(low[u], dfn[j]);
	}
}

int main()
{
	scanf("%d%d", &n, &m);
	
	while (m -- )
	{
		int a, b;
		scanf("%d%d", &a, &b);
		if (a == b) continue; // 判掉自环 
		add(a, b), add(b, a);
	}
	
	for (int i = 1; i <= n; i ++ ) // 图中可能存在多个连通块 
		if (!dfn[i])
			tarjan(i, i);
			
	printf("%d\n", cnt);
	for (int i = 1; i <= cnt; i ++ ) {
		printf("%d ", res[i].size());
		for (int j : res[i])
			printf("%d ", j);
		puts("");
	}
	
	return 0;
}

完结撒花✿✿ヽ(°▽°)ノ✿

posted @ 2022-07-15 10:43  Link-Cut-Y  阅读(198)  评论(0编辑  收藏  举报