双联通分量

边双联通分量

在图论中,边双连通分量(Edge Biconnected Component,通常简称 e-BCC)描述的是一种具有高度可靠性的网络拓扑结构。

定义

简单来说,边双连通分量是指不包含任何“桥”(割边)的极大连通子图。这意味着在这个子图内部,无论你切断哪一条边,子图内的所有节点依然保持连通。

三大关键性质

  1. 在同一个 e-BCC 中,任意两个节点之间,必定存在至少两条完全不共享任何边(边不相交)的路径。
  2. 这是它与点双连通分量(v-BCC)最大的区别:在点双中,割点可以同时属于多个分量;但在边双中,每一个节点都恰好属于唯一一个 e-BCC
  3. 图中的“桥”不属于任何一个边双连通分量,它们的唯一作用是连接不同 e-BCC。

求法

采用两遍 DFS。

  1. 第一遍 DFS(跑 Tarjan 找桥):
  • 遍历图,维护 \(dfn\)\(low\) 数组。
  • 当发现某条边 \((u, v)\) 满足 \(low[v] > dfn[u]\) 时,说明 \(v\) 往下走绝对绕不回 \(u\) 的上方,这条边就是“桥”。
  • 用一个布尔数组将这条边特殊标记下来。
  1. 第二遍 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;
}

posted on 2026-08-25 21:37  easy42  阅读(4)  评论(0)    收藏  举报