无向图的双连通分量
定义
在无向图中,一个双连通分量(Biconnected Component, BCC)是指这样的子图:删除其中任意一个顶点都不会使这个子图分离成两个或更多个不相连的子图。换句话说,双连通分量是无割点的极大连通子图。
运用情况
-
网络可靠性分析:在通信网络中,理解网络的连通性,找出关键的节点和链路,有助于提升网络的稳定性和容错能力。
-
电路设计:在电路板设计中,识别哪些部分是双连通的可以帮助工程师优化布局,减少因单一故障点导致的整体失效风险。
-
社交网络分析:在社交网络中,双连通分量可以帮助识别社群内部的关键人物或纽带,这些人物对于社群的凝聚力至关重要。
注意事项
-
割点:双连通分量的计算过程中,割点(即删除该点会使得图分裂成多个连通分量的点)的识别非常重要。每个割点都是连接不同双连通分量的桥梁。
-
边的冗余:双连通分量中的每条边至少属于一个双连通分量,但可能同时属于多个分量,尤其是那些连接割点的边。
-
数据结构:在算法实现时,合理选择数据结构(如邻接表、并查集等)对提高算法效率至关重要。
解题思路
-
DFS遍历:通过深度优先搜索(DFS)遍历图中的所有顶点,为每个顶点分配发现时间和低值(low value),类似于Tarjan算法中寻找强连通分量的过程。
-
识别割点和双连通分量:
- 对于每个顶点u,在其DFS子树中,如果存在一个子节点v,使得v到其所有祖先的路径都经过u,则u是一个割点。
- 当发现一个顶点u的低值等于其发现时间时,意味着找到了一个双连通分量的边界。
-
记录双连通分量:每当找到一个双连通分量的边界,就记录当前DFS栈中的顶点集合作为该双连通分量的一部分。
AcWing 395. 冗余路径
题目描述
395. 冗余路径 - AcWing题库
运行代码
#include <cstring>
#include <iostream>
using namespace std;
const int N = 5010, M = 20010;
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;
int d[N];
bool is_bridge[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
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(dfn[u] < low[j])
is_bridge[i] = is_bridge[i ^ 1] = true;
}
else if(i != (from ^ 1))
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(u != y);
}
}
int main()
{
while(cin >> n >> m, n || m)
{
memset(h, -1, sizeof h);
idx = 0;
while(m -- )
{
int a, b;
cin >> a >> b;
add(a, b);
add(b, a);
}
tarjan(1, -1);
for(int i = 0; i < idx; i ++ )
if(is_bridge[i])
d[id[e[i]]] ++ ;
int cnt = 0;
for(int i = 1; i <= dcc_cnt; i ++ )
if(d[i] == 1)
cnt ++ ;
cout << (cnt + 1) / 2 << endl;
return 0;
}
}
代码思路
- 首先定义了一些常量和数组来存储图的相关信息,如节点数、边数、邻接表等。
add
函数用于向邻接表中添加边。tarjan
函数是核心,通过深度优先搜索来计算每个节点的dfn
(时间戳)和low
(回溯能到达的最早时间戳)值,从而判断边是否为桥,并标记双连通分量。- 在
main
函数中,读取输入的节点数和边数,构建图,调用tarjan
函数进行计算,最后统计双连通分量的相关信息并输出结果。
改进思路
- 代码的注释可以更加详细,以提高代码的可理解性。
- 可以增加一些错误处理,比如输入不合法时的提示。
- 考虑使用更具可读性的数据结构,如
vector
来替代数组。
改进代码
#include <iostream>
#include <vector>
using namespace std;
// 最大节点数
const int N = 5010;
// 最大边数
const int M = 20010;
int n, m;
// 邻接表
vector<int> h(N, -1);
vector<int> e(M);
vector<int> ne(M);
int idx;
// 时间戳
int dfn[N];
// 回溯能到达的最早时间戳
int low[N];
int timestamp;
// 栈
vector<int> stk;
int top;
// 每个节点所属的双连通分量编号
int id[N];
int dcc_cnt;
// 每个双连通分量的度
int d[N];
// 标记边是否为桥
bool is_bridge[M];
// 添加边
void add(int a, int b) {
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
/**
* Tarjan 算法核心函数
* @param u 当前节点
* @param from 边的编号
*/
void tarjan(int u, int from) {
dfn[u] = low[u] = ++timestamp;
stk.push_back(u);
for (int i = h[u]; i!= -1; i = ne[i]) {
int j = e[i];
if (!dfn[j]) {
tarjan(j, i);
low[u] = min(low[u], low[j]);
if (dfn[u] < low[j])
is_bridge[i] = is_bridge[i ^ 1] = true;
}
else if (i!= (from ^ 1))
low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u]) {
++dcc_cnt;
int y;
do {
y = stk.back();
stk.pop_back();
id[y] = dcc_cnt;
} while (u!= y);
}
}
/**
* 主函数
* 处理输入,计算并输出结果
*/
int main() {
while (cin >> n >> m, n || m) {
// 初始化
fill(h.begin(), h.end(), -1);
idx = 0;
while (m--) {
int a, b;
cin >> a >> b;
add(a, b);
add(b, a);
}
tarjan(1, -1);
for (int i = 0; i < idx; i++)
if (is_bridge[i])
d[id[e[i]]]++;
int cnt = 0;
for (int i = 1; i <= dcc_cnt; i++)
if (d[i] == 1)
cnt++;
cout << (cnt + 1) / 2 << endl;
return 0;
}
}