割边
割边(桥)的判定与实现:Tarjan 算法求无向图中删除后使图不连通的边。
一句话算法
割边判定看 low[v] > dfn[u]:如果 v 的子树连 u 都回不到,那么边 u-v 就是唯一通道。
问题模型
给定一张无向图。删除某条边后,如果图的连通块数量增加,那么这条边叫做割边,也叫桥。
割边反映的是无向图中的关键连接:
- 删除它会切断一部分点;
- 它不在任何环上;
- 它常用于边双连通分量和网络可靠性分析。
核心直觉
对 DFS 树中的树边 u -> v:
- 如果
v的子树能通过返祖边回到u或u的祖先,那么边u-v不唯一; - 如果
v的子树完全回不到u或更早位置,那么u-v是唯一通道。
因此割边条件是:
注意这里必须是 >,不是 >=。如果 low[v] == dfn[u],说明 v 的子树还能回到 u,边 u-v 在环上,不是桥。
算法步骤
- 对每个未访问点 DFS。
- 进入点
u时,令dfn[u] = low[u] = ++timer。 - 枚举边编号
i,终点为v。 - 不走进入边的反向边
i == (in_edge ^ 1)。 - 如果
v未访问:- 递归 DFS;
- 用
low[v]更新low[u]; - 若
low[v] > dfn[u],则边i和i^1是桥。
- 如果
v是祖先,用dfn[v]更新low[u]。
算法证明
核心不变量:low[u] 表示 u 的子树能通过返祖边到达的最早时间戳。
- 若
low[v] > dfn[u],说明v子树无法到达u或u的任何祖先。 - 那么从
v子树到外部,唯一经过 DFS 树的边就是u-v。 - 删除
u-v后,v子树与外部断开,所以u-v是割边。 - 若
low[v] <= dfn[u],说明存在从v子树回到u或祖先的边,删除u-v仍有替代路径,所以不是割边。
因此判定条件正确。
复杂度分析
设点数为
- 时间复杂度:
。 - 空间复杂度:
。
代码实现
cpp
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
// 与求割点不同,求割边时有两个关键点需要注意:
// 1.判定条件更严格:low[v] > dfn[u] (不能取等号)。
// 因为如果 low[v] == dfn[u],说明 $v$ 还能回到 $u$,那么 $u-v$ 这条边在环上,
// 不是桥。防止走回头路:在无向图中,判定“不走回头路”时,
// 2.不能简单地判断 v != father,因为两个点之间可能存在重边(多条边)。
// 我们需要通过边的编号来判断——即“不要走刚才进来的那条边的反向边”。
struct TarjanBridge {
int n, timer;
int dfn[maxn], low[maxn];
bool is_bridge[maxe * 2]; // 标记边是否为桥 (注意大小是边数)
void set(int _n) {
n = _n;
timer = 0;
memset(dfn, 0, sizeof(dfn));
memset(low, 0, sizeof(low));
memset(is_bridge, 0, sizeof(is_bridge));
}
/*
* u: 当前节点
* in_edge: 进入 u 点的那条边的编号 (替代 father 参数)
* 用于处理重边情况,确保只屏蔽刚才过来的那一条具体的边
*/
void dfs(int u, int in_edge) {
dfn[u] = low[u] = ++timer;
// 遍历以 u 为起点的所有边
for (int i = e.h[u]; i != -1; i = e[i].next) {
int v = e[i].v;
// 判定条件:i 不是“进入 u 的那条边”的反向边
// in_edge 是进入 u 的边,in_edge ^ 1 是从 u 回去父亲的边
// 过滤掉刚才过来的那条边的反向边
if( i == (in_edge ^ 1) ) continue;
if (!dfn[v]) { // 树枝边
dfs(v, i); // 将当前边 i 传入下一层
low[u] = std::min(low[u], low[v]);
// 核心:割边判定
// 必须是 >,不能是 >=。
// 如果 low[v] > dfn[u],说明 v 及其子树无法回到 u 或 u 的祖先
//
// 这里的 else if (dfn[v] < dfn[u]) 其实是个很好的防御性编程。
// 在无向图 DFS 树中,如果 v 已访问且不是父亲,它一定是祖先 (dfn[v] < dfn[u])。
// 唯一的例外是有重边连接到由 u 刚访问过的子节点(此时 dfn[v] > dfn[u]),
// 但这种情况不需要更新 low[u],因为子节点的 low 已经处理过这种情况了。
if (low[v] > dfn[u]) {
is_bridge[i] = is_bridge[i ^ 1] = true;
}
}
else if( dfn[v] < dfn[u] ) { // 回边
low[u] = std::min(low[u], dfn[v]);
}
}
}
void solve() {
// 遍历所有连通分量
for (int i = 1; i <= n; i++) {
if (!dfn[i]) {
// 对于根节点,没有“进入的边”,传入 -1
// -1 ^ 1 结果是一个很大的负数或特定值,肯定不会等于正常的边索引(>=0)
dfs(i, -1);
}
}
}
// 辅助:获取所有桥 (以 u < v 形式返回)
std::vector<std::pair<int, int>> get_bridges() {
std::vector<std::pair<int, int>> ans;
// edge_cnt 是总边数索引,遍历 0 到 edge_cnt-1
// 每次 += 2 跳过反向边,避免重复添加
for (int i = 0; i < edge_cnt; i += 2) {
if (is_bridge[i]) {
ans.push_back({e[i ^ 1].v, e[i].v}); // u=e[i^1].v, v=e[i].v
}
}
return ans;
}
};
测试用例
输入图:
4 4
1 2
2 3
3 1
3 4
割边是:
3 4
前三个点构成环,环上的边都不是桥;3-4 是点 4 通向图中其他点的唯一边。
应用分类详解
割边用于识别无向图中的关键连接。
一、网络关键边
典型模式: 删除一条边后网络断开。
识别信号: 题面出现“桥”“关键道路”“断开一条边”。
核心建模: 把道路/线路看作边,求所有桥。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 桥模板 | poj-3352 | 求桥后考虑加边 |
| 网络可靠性 | 通信线路类题 | 找删除后会断开的边 |
二、边双连通分量
典型模式: 缩掉所有非桥边形成边双。
识别信号: 出现“任意两点之间有两条边不重复路径”。
核心建模: 桥是不同边双之间的边界。
经典例题
1. poj-3352
经典桥与边双题。先求桥,再分析缩点后的叶子数量。
2. luogu-P1656
无向图求桥并按字典序输出。
3. 边双连通分量模板
割边是理解 e-BCC 的前置知识。继续学习本书边双连通分量章节。