浅谈点双连通分量

本文章同步发表于本人 luogu 博客。

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

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

什么是割点

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

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

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

什么是点双连通分量

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

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

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

怎样求点双连通分量

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

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

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

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

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


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

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

在无向图的双联通分量里,对于两个点 x,yy 表示与 x 直接相连的一个点)。如果 lowydfnx,就说明 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 @   Link-Cut-Y  阅读(203)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示
点击右上角即可分享
微信分享提示