洛谷P8436 【模板】 边双连通分量 题解
P8436 【模板】 边双连通分量
前言
本文聊一聊边双连通分量是什么、如何求边双连通分量。
概念
割边(桥):在一个联通的无向图中,若删掉某条边,使得这个无向图不连通,那么这条边就称为“割边(桥)”,一个无向图中可能有不止一条割边。
双联通子图:在原图的某个子图 \(G'\) 中,不存在割边,那么 \(G'\) 就为双联通子图。
边双连通分量:原图中的极大双联通子图就为边双连通分量。形式化地,若某个双联通子图 \(G'\), 不存在其他的一个双联通子图 \(G''\),使得 \(G' \subsetneq G''\),那么 \(G'\) 就为边双连通分量,简称“e-DCC”。
找割边
找到边双连通分量的第一步是设法找到割边,颜值超级超级高的 tarjan 给出了很巧妙的求解方法。
不难发现,割边的两个端点,只有一条路径相连(这是定义嘛),也就是说,如果我们吧这个路径堵住,那么它们就不会联通。
代码该怎样写?
-
引入一个时间戳,表示对应节点在遍历时是第几个被访问到的,每到达一个点先给这个点附上时间戳的值,随后时间戳 +1,等待下一次赋值。
-
对于当前点 \(u\) ,令上一次遍历时到达到它的有向边为 \(e\),我们遍历其除了 \(e\) 的反向边以外的所有连边,如果某条边是割边,那么这条边遍历的点一定到不了之前遍历过的点。对于每个能到达的点,我们更新对应点的时间戳,使其时间戳尽量小(取最小值操作),如果某个点更新完毕后的时间戳不大于更新前的对应点的时间戳,说明这两个点之间的边就是割边。
代码如下
void tarjan(int u, int from)
{
dfn[u] = low[u] = ++ timestamp; //timestamp为时间戳
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j, i);
low[u] = min(low[u], low[j]);
if (low[j] > dfn[u])
is_bridge[i] = is_bridge[i ^ 1] = true; //is_bridge来维护i是否为桥
}
else if (i != (1 ^ from)) //不能走反向边
low[u] = min(low[u], dfn[j]); //更新时间戳
}
return;
}
找边双连通分量
有上面的基础,这个就简单了。
如果经过一通更新发现它的时间戳没有变化,说明这个点已经是边界了。
维护一个栈来统计遍历的点数,到达边界后弹出直至弹到最初遍历节点,所弹的所有节点构成一个边双连通分量。
代码如下
void tarjan(int u, int from)
{
dfn[u] = low[u] = ++ timestamp;
stk[ ++ top] = u;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j, i);
low[u] = min(low[u], low[j]);
if (low[j] > dfn[u])
is_bridge[i] = is_bridge[i ^ 1] = true;
}
else if (i != (1 ^ from))
low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u])
{
dcc_cnt ++ ; //dcc_cnt为边双连通分量编号
int y;
do
{
y = stk[top -- ];
id[y] = dcc_cnt; //id为某点的所在边双连通分量编号
} while (y != u);
}
return;
}
统计答案
这个问题,对于总的边双连通分量个数,答案就为 dcc_cnt 的值;而对于每一个边双连通分量中的点的个数,我们开个数组 tot 来维护,这个很简单;对于节点编号,我们之前的 id 数组起到了至关重要的作用,在此基础上再用一个 vector 来存答案即可。(详见代码)
Code
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
const int N = 500010, M = 2000010 * 2;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
int id[N], dcc_cnt;
bool is_bridge[M];
int tot[N];
vector <int> ans[N];
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++ ;
return;
}
void tarjan(int u, int from)
{
dfn[u] = low[u] = ++ timestamp;
stk[ ++ top] = u;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j, i);
low[u] = min(low[u], low[j]);
if (low[j] > dfn[u])
is_bridge[i] = is_bridge[i ^ 1] = true;
}
else if (i != (1 ^ from))
low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u])
{
dcc_cnt ++ ;
int y;
do
{
y = stk[top -- ];
id[y] = dcc_cnt;
} while (y != u);
}
return;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 1; i <= m; i ++ )
{
int a, b;
cin >> a >> b;
add(a, b);
add(b, a);
}
for (int i = 1; i <= n; i ++ )
if (!dfn[i]) tarjan(i, -1);
cout << dcc_cnt << endl;
for (int i = 1; i <= n; i ++ )
{
tot[id[i]] ++ ;
ans[id[i]].push_back(i);
}
//统计答案
int p = 1;
while (tot[p])
{
cout << tot[p] << ' ';
for (int i = 0; i < ans[p].size(); i ++ )
cout << ans[p][i] << ' ';
p ++ ;
cout << endl;
}
return 0;
}
后语
应用练习 P2860