浅谈点双连通分量
本文章同步发表于本人 \(\texttt{luogu}\) 博客。
文章就点双连通分量进行讲解。
什么是割点
在一个无向联通图 \(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);
}
另外,还需要注意一下下面这些地方:
-
一个割点可能属于许多个点双连通分量。因此弹栈时不能直接弹出割点。
-
可能存在孤立点。孤立点也属于点双连通分量,需要特判。
-
数据存在自环(
好恶心)
完整代码如下:
// 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;
}
完结撒花✿✿ヽ(°▽°)ノ✿