双联通分量
边双联通分量
在图论中,边双连通分量(Edge Biconnected Component,通常简称 e-BCC)描述的是一种具有高度可靠性的网络拓扑结构。
定义
简单来说,边双连通分量是指不包含任何“桥”(割边)的极大连通子图。这意味着在这个子图内部,无论你切断哪一条边,子图内的所有节点依然保持连通。
三大关键性质
- 在同一个 e-BCC 中,任意两个节点之间,必定存在至少两条完全不共享任何边(边不相交)的路径。
- 这是它与点双连通分量(v-BCC)最大的区别:在点双中,割点可以同时属于多个分量;但在边双中,每一个节点都恰好属于唯一一个 e-BCC。
- 图中的“桥”不属于任何一个边双连通分量,它们的唯一作用是连接不同 e-BCC。
求法
采用两遍 DFS。
- 第一遍 DFS(跑 Tarjan 找桥):
- 遍历图,维护 \(dfn\) 和 \(low\) 数组。
- 当发现某条边 \((u, v)\) 满足 \(low[v] > dfn[u]\) 时,说明 \(v\) 往下走绝对绕不回 \(u\) 的上方,这条边就是“桥”。
- 用一个布尔数组将这条边特殊标记下来。
- 第二遍 DFS(避开桥找分量):
- 清空访问标记,再次遍历所有节点。
- 执行普通的 DFS 或 BFS,遇到被标记为“桥”的边,绝对不走。
- 这样一来,每一次搜索能走到的所有节点,就构成了一个边双连通分量。
代码(AI 注释)
洛谷 P8436 的代码。
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n, m; // n: 节点数, m: 边数
int pcnt; // 边的编号计数器,用于给每条边分配唯一编号
// 边的结构体
struct edge{
int vv, cnt; // vv: 边的目标节点, cnt: 边的唯一编号
};
vector<edge> v[500005]; // 邻接表存图
// Tarjan 算法相关的变量
int dfn[500005]; // dfn[x]: 节点 x 在 DFS 中被访问的次序(时间戳)
int low[500005]; // low[x]: 节点 x 通过最多一条返祖边能到达的最小 dfn 值
int cnt; // 用于生成时间戳
bool b[2000005]; // b[i]=1 表示编号为 i 的边是一条“桥”(割边)
// 提取连通分量相关的变量
int bcc[500005]; // bcc[x] 记录节点 x 属于哪一个边双连通分量
int gcnt; // 边双连通分量的总数量
vector<int> h[500005]; // h[i] 存储第 i 个边双连通分量包含的所有节点
// 第一遍 DFS:Tarjan 算法核心,用于寻找图中的所有“桥”
// x: 当前节点, f: 从上一个节点走到当前节点所经过的【边的编号】
void tarjan(int x, int f){
// 初始化时间戳和追溯值
dfn[x] = low[x] = ++cnt;
// 遍历当前节点 x 的所有出边
for(int i = 0; i < v[x].size(); i++){
// 1. 如果这条边是刚刚走过来的那条边,直接跳过,防止在无向图中原路返回
if(v[x][i].cnt == f) continue;
// 2. 如果目标节点还没有被访问过(这是一条树边)
if(!dfn[v[x][i].vv]){
// 继续往下递归 DFS
tarjan(v[x][i].vv, v[x][i].cnt);
// 用子节点能到达的最小时间戳更新自己的 low 值
low[x] = min(low[x], low[v[x][i].vv]);
// 【核心判定】:如果子节点 v 怎么绕都无法回到 x 或 x 的祖先
// 说明 x 到 v 的这条边是唯一的通道,因此这条边就是“桥”
if(low[v[x][i].vv] > dfn[x]){
b[v[x][i].cnt] = 1; // 将该边标记为桥
}
}
// 3. 如果目标节点已经被访问过了(这是一条返祖边)
else {
// 用祖先节点的时间戳更新自己的 low 值
low[x] = min(low[x], dfn[v[x][i].vv]);
}
}
}
// 第二遍 DFS:避开所有桥,提取出边双连通分量
void dfs(int x){
bcc[x] = gcnt; // 标记节点 x 属于当前的连通分量 gcnt
h[gcnt].push_back(x); // 将节点 x 加入当前分量的集合中
for(int i = 0; i < v[x].size(); i++){
// 如果这条边是桥,或者目标节点已经被归入某个分量了,就不走这条边
if(b[v[x][i].cnt] || bcc[v[x][i].vv]) continue;
// 否则,说明目标节点和当前节点属于同一个边双连通分量,继续 DFS
dfs(v[x][i].vv);
}
}
signed main(){
// 读入节点数和边数
cin >> n >> m;
for(int i = 1; i <= m; i++){
int u, vv;
cin >> u >> vv;
++pcnt; // 为每条无向边分配一个全局唯一编号
// 无向图建边,双向都存,但赋予相同的边编号 pcnt
v[u].push_back({vv, pcnt});
v[vv].push_back({u, pcnt});
}
// 1. 跑 Tarjan 找桥
// 考虑到图可能不连通(有多个连通块),所以要遍历所有节点
for(int i = 1; i <= n; i++){
if(!dfn[i]) tarjan(i, 0); // 0 表示起点没有 incoming edge
}
// 2. 跑普通 DFS 提取分量
// 同样遍历所有节点,确保所有连通块都被处理到
for(int i = 1; i <= n; i++){
if(!bcc[i]){
++gcnt; // 开启一个新的边双连通分量
dfs(i);
}
}
// 3. 输出结果
cout << gcnt << endl; // 输出边双连通分量的总数
for(int i = 1; i <= gcnt; i++){
cout << h[i].size() << " "; // 输出该分量内的节点数
for(int j = 0; j < h[i].size(); j++){
cout << h[i][j] << " "; // 输出该分量内的所有节点编号
}
cout << endl;
}
return 0;
}
浙公网安备 33010602011771号