点双连通分量
为什么不和cut-node 一样判断 root ?
思考下面的图
2
/ \
/ \
1 ----- 3
求 点双连通分量 (v-BCC) 和求 强连通分量 (SCC) 或 边双连通分量 (e-BCC) 在栈操作上的最核心区别。
核心结论: 绝对不能把割点 u 出栈,因为一个割点 u 可能同时属于多个点双连通分量。它必须留在栈里(或者说留在递归结构中),作为后续其他子树(其他 BCC)的“连接点”。
@include_md(“./模拟练习v-bcc.md”);
模板代码
cpp
copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/*
代码细节解释:
0. 此代码同时点双连通分量 和 割点. 因为 求 v-bcc 的时候,通常都会问: 哪些点是割点
1. **`std::vector<int> bcc[maxn]`**:
* 与 SCC 不同,SCC 中每个点只属于一个分量,可以用 `id[u]` 数组标记。
* 在点双 (v-BCC) 中,**割点**会同时属于多个 BCC。因此,我们通常用 `vector` 列表来保存每个 BCC 里有哪些点,而不是给每个点打唯一的 ID 标记。
2. **`st.push(u)` 与出栈逻辑**:
* 我们将点入栈。
* 当满足 `low[v] >= dfn[u]` 时,说明找到了一个以 `u` 为“顶端”的双连通分量。
* 我们不断 `pop` 直到弹出 `v`。
* **关键点**:`u` 也是这个分量的一部分,需要 `bcc[...].push_back(u)`,但是 **`u` 不能出栈**。因为 `u` 还是它父节点所在 BCC 的一部分(如果 `u` 不是根),或者是其他子树分支 BCC 的分割点。
3. **`e(u)`**:
* 这里保留了你代码中的 `e(u)` 写法,假设你已经定义了类似 `#define e(u) head[u]` 或者相应的函数来获取邻接表头指针。
这是一个求 **点双连通分量 (v-BCC)** 的模板。
主要区别在于:
1. **无向图 DFS**:需要传入 `father` 参数防止走回头路(或通过边索引判断)。
2. **出栈时机**:SCC 是在回溯完 `u` 后判断 `low[u] == dfn[u]` 出栈;而 BCC 是在处理子节点 `v` 时,若发现 `low[v] >= dfn[u]`,则说明 `v` 及其子树无法绕过 `u` 到达更早的祖先,此时 `u` 和 `v` 的子树构成一个点双。
3. **割点特性**:一个割点 (Articulation Point) 可以属于多个点双连通分量。
*/
struct TarjanBCC {
int n, timer;
std::stack<int> st;
int dfn[maxn], low[maxn];
int bcc_cnt; // BCC 计数
bool is_cut[maxn];
int root; //记录根节点
std::vector<int> bcc[maxn]; // 存储每个 BCC 包含的具体节点
void set(int _n) {
n = _n;
timer = bcc_cnt = 0;
memset(dfn, 0, sizeof(dfn));
memset(low, 0, sizeof(low));
memset(is_cut,0,sizeof(is_cut));
while (!st.empty()) st.pop();
for (int i = 0; i <= n; i++) bcc[i].clear();
}
// 无向图,需要 fa 参数防止直接走回父节点
void dfs(int u, int fa) {
dfn[u] = low[u] = ++timer;
st.push(u);
int child = 0;
for (int i = e.h[u]; ~i; i = e[i].next) {
int v = e[i].v;
if (v == fa) continue; // 无向图核心:不走回头路
if (!dfn[v]) { // 如果 v 没被访问过
dfs(v, u);
child++;
if(u != root && low[v] >= dfn[u]) {
is_cut[u] = 1;
}
// 更新 low 值
low[u] = std::min(low[u], low[v]);
// 核心判断:low[v] >= dfn[u] 说明 v 没法回到 u 的祖先
// 此时 u 是割点(或者根),u-v 这条边及其下方的点构成一个 BCC
if (low[v] >= dfn[u]) {
bcc_cnt++;
while (true) {
int node = st.top(); st.pop();
bcc[bcc_cnt].push_back(node);
if (node == v) break; // 只要弹到 v 为止
}
// 注意:u 也是这个 BCC 的一部分,但 u 可能属于多个 BCC,
// 所以 u 不能出栈,只是把 u 加入到当前 BCC 列表中
bcc[bcc_cnt].push_back(u);
}
} else if (dfn[v] < dfn[u]) { // 返祖边
low[u] = std::min(low[u], dfn[v]);
}
}
if( u == root && child > 1)
is_cut[u] = 1;
}
void solve() {
for (int i = 1; i <= n; i++) {
if (!dfn[i] && e.h[i] != -1 ) {
// 此时栈为空,dfs 根节点
// 根节点的特判通常包含在上述 dfs 逻辑中
// 只有当图中有孤立点时,需额外注意栈内残留
root = i;
dfs(i, 0);
// 如果是孤立点或者根节点处理完栈里还有元素(极少见情况,视题目定义而定)
// 实际上标准 v-BCC 逻辑中,上述 dfs 里的 if (low[v] >= dfn[u]) 会处理所有连通块
// 唯一的例外是如果 i 是一个孤立点(没有边的点),它不会进入循环
// 如果需要记录孤立点为 BCC,可以在这里补判
}
}
}
// helper
int cut_cnt() {
int cnt = 0;
for(int i = 1;i <= n ;++i ) // i: 1->n
cnt += is_cut[i];
return cnt;
}
};
边双连通分量
cpp
copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 假设外部已经有了前向星/邻接表结构体 e 和 head 数组
// struct Edge { int v, next; } e[maxe]; int h[maxn];
struct TarjanEBCC {
int n, timer;
std::stack<int> st;
int dfn[maxn], low[maxn];
int bcc_cnt; // e-BCC 计数
int bcc_id[maxn]; // 记录每个点属于哪个 e-BCC (1 ~ bcc_cnt)
// 初始化
void set(int _n) {
n = _n;
timer = bcc_cnt = 0;
memset(dfn, 0, sizeof(dfn));
memset(low, 0, sizeof(low));
memset(bcc_id, 0, sizeof(bcc_id));
while (!st.empty()) st.pop();
}
// e-BCC 的 DFS
// 参数 fa: 父节点,用于防止直接沿反向边回去 (处理无向图)
void dfs(int u, int fa) {
dfn[u] = low[u] = ++timer;
st.push(u);
for (int i = e.h[u]; ~i; i = e[i].next) {
int v = e[i].v;
if (v == fa) continue; // 无向图核心:不走回头路 (若有重边需改用边下标判断)
if (!dfn[v]) {
dfs(v, u);
low[u] = std::min(low[u], low[v]);
} else {
low[u] = std::min(low[u], dfn[v]);
}
}
// 核心区别:当 low[u] == dfn[u] 时,说明 u 是该连通分量的“根”
// 此时栈中 u 及以上的点构成一个完整的 e-BCC
if (low[u] == dfn[u]) {
bcc_cnt++;
while (true) {
int node = st.top();
st.pop();
bcc_id[node] = bcc_cnt; // 标记该点属于哪个分量
if (node == u) break; // 把该分量的点都弹出了
}
}
}
// 入口函数
void solve() {
for (int i = 1; i <= n; i++) {
if (!dfn[i]) {
dfs(i, 0);
}
}
}
};