连通性问题
Tarjan 连通性算法模板
比赛速查版:覆盖有向图强连通分量、无向图桥与边双连通分量、割点与点双连通分量。代码使用 C++17。
0. 术语与 low-link
以下实现使用:
dfn[u]:DFS 首次访问u的时间戳。low[u]:从u的 DFS 子树出发,沿 DFS 树边向下,再至多使用一条合适的非树边,能到达的最早祖先的dfn。- 无向图邻接表保存
(邻点, 边编号)。用边编号跳过父边,因此支持平行边;只跳过父节点会把平行边误判。
low 的更新取决于问题:
- 有向图 SCC:只用仍在 Tarjan 栈中的已访问点更新
low。 - 无向图桥、割点:对返祖边
u -> v,通常只在dfn[v] < dfn[u]时用dfn[v]更新;不能把已经完成的 DFS 子树当作返祖边。
Tarjan 算法整体为 \(O(V+E)\)。下方递归版代码要求 DFS 深度能承受运行环境的栈;极深图应改成显式栈迭代 DFS。
1. 有向图:强连通分量 SCC
强连通分量是极大的点集,其中任意两点互相可达。Tarjan 用一个栈保存尚未归属最终 SCC 的点。
当 low[u] == dfn[u] 时,u 是当前 SCC 的根;从栈顶弹出直到 u,这些点构成一个 SCC。不同 SCC 缩点后得到 DAG。
模板:SCC 编号、分量大小
struct SCC {
int n, timer = 0, scc_count = 0;
vector<vector<int>> g;
vector<int> dfn, low, stk, comp, comp_size;
vector<char> in_stack;
explicit SCC(int n)
: n(n), g(n), dfn(n), low(n), comp(n, -1), in_stack(n) {}
void add_edge(int u, int v) {
g[u].push_back(v);
}
void dfs(int u) {
dfn[u] = low[u] = ++timer;
stk.push_back(u);
in_stack[u] = true;
for (int v : g[u]) {
if (!dfn[v]) {
dfs(v);
low[u] = min(low[u], low[v]);
} else if (in_stack[v]) {
// 只允许通过仍在栈中的点回到未完成部分。
low[u] = min(low[u], dfn[v]);
}
}
if (low[u] == dfn[u]) {
int id = scc_count++;
int size = 0;
while (true) {
int v = stk.back();
stk.pop_back();
in_stack[v] = false;
comp[v] = id;
++size;
if (v == u) break;
}
comp_size.push_back(size);
}
}
int run() {
for (int u = 0; u < n; ++u) {
if (!dfn[u]) dfs(u);
}
return scc_count;
}
};
使用约定:顶点编号为 0..n-1。调用 run() 后,comp[u] 是 SCC 编号,comp_size[id] 是该 SCC 的点数。编号顺序取决于 DFS,不应假设特定方向的拓扑序;若需要拓扑序,对缩点 DAG 再做拓扑排序。
缩点建 DAG
vector<vector<int>> dag(scc.scc_count);
for (auto [u, v] : edges) {
int a = scc.comp[u];
int b = scc.comp[v];
if (a != b) dag[a].push_back(b);
}
如果后续算法要求 DAG 中没有重边,可排序并去重每个邻接表,或用集合去重。SCC 解决强连通;若只问忽略方向后的弱连通分量,应把有向边当无向边后 DFS/并查集,不需要 SCC。
2. 无向图:桥与边双连通分量
桥是删除后会增加连通分量数的边。对 DFS 树边 (u,v),其中 u 是父亲、v 是儿子:
若 low[v] == dfn[u],说明子树能通过返祖边回到 u,该树边不是桥。
删除所有桥后得到的连通分量称为边双连通分量(2-edge-connected components)。把每个分量缩成一点、每座桥缩成一条边,所得图是森林;原图连通时是树,称桥树。
模板:找桥、边双染色、桥森林
struct BridgeDecomposition {
struct Edge { int to, id; };
int n, m = 0, timer = 0, comp_count = 0;
vector<vector<Edge>> g;
vector<pair<int, int>> edges;
vector<int> dfn, low, comp, comp_size;
vector<char> is_bridge;
vector<vector<int>> bridge_forest;
explicit BridgeDecomposition(int n)
: n(n), g(n), dfn(n), low(n), comp(n, -1) {}
int add_edge(int u, int v) {
int id = m++;
edges.push_back({u, v});
g[u].push_back({v, id});
g[v].push_back({u, id});
return id;
}
void dfs_bridge(int u, int parent_edge) {
dfn[u] = low[u] = ++timer;
for (auto [v, id] : g[u]) {
if (id == parent_edge) continue;
if (!dfn[v]) {
dfs_bridge(v, id);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u]) is_bridge[id] = true;
} else {
// 只用指向祖先的非树边更新。
low[u] = min(low[u], dfn[v]);
}
}
}
void paint(int start, int id) {
stack<int> st;
st.push(start);
comp[start] = id;
while (!st.empty()) {
int u = st.top();
st.pop();
++comp_size[id];
for (auto [v, eid] : g[u]) {
if (is_bridge[eid] || comp[v] != -1) continue;
comp[v] = id;
st.push(v);
}
}
}
int run() {
is_bridge.assign(m, false);
for (int u = 0; u < n; ++u) {
if (!dfn[u]) dfs_bridge(u, -1);
}
comp_count = 0;
comp_size.clear();
for (int u = 0; u < n; ++u) {
if (comp[u] != -1) continue;
comp_size.push_back(0);
paint(u, comp_count++);
}
bridge_forest.assign(comp_count, {});
for (int id = 0; id < m; ++id) {
if (!is_bridge[id]) continue;
auto [u, v] = edges[id];
int a = comp[u], b = comp[v];
bridge_forest[a].push_back(b);
bridge_forest[b].push_back(a);
}
return comp_count;
}
};
支持断开图、平行边和孤立点。自环不会成为桥,且代码可处理自环;题目若有特殊多重边定义,仍应以题面为准。调用 run() 后,is_bridge[id]、comp[u]、comp_size[id] 和 bridge_forest 可用。
常见结论:连通无向图是边双连通(任意两点间有两条边不相交路径)当且仅当没有桥。这里说的是边不相交,不是点不相交。
3. 无向图:割点与点双连通分量
割点是删除后会增加连通分量数的顶点。对 DFS 树边 (u,v):
- 若
u不是 DFS 根,u是割点当且仅当存在儿子v满足low[v] >= dfn[u]。 - 若
u是 DFS 根,它是割点当且仅当 DFS 树儿子数至少为 \(2\)。
注意等号:low[v] == dfn[u] 时,子树只能回到 u,删掉 u 仍会断开,所以 u 是割点。
点双连通分量(vertex-biconnected components / blocks)通常按边划分:每条非自环边恰属于一个点双分量;不同分量可以共享割点。下面的模板用边栈,在 low[v] >= dfn[u] 时弹出一个分量。点双分量里收集的顶点会去重。
模板:割点、点双分量、圆方树
该模板假设图无自环;支持平行边和断开图。孤立点会作为单点点双分量输出。
struct VertexBiconnected {
struct Edge { int to, id; };
int n, m = 0, timer = 0;
vector<vector<Edge>> g;
vector<pair<int, int>> edges;
vector<int> dfn, low;
vector<char> is_cut;
vector<int> edge_stack;
vector<vector<int>> blocks; // 每个点双分量的顶点集合
vector<int> mark;
int mark_id = 0;
explicit VertexBiconnected(int n)
: n(n), g(n), dfn(n), low(n), is_cut(n), mark(n) {}
int add_edge(int u, int v) {
int id = m++;
edges.push_back({u, v});
g[u].push_back({v, id});
g[v].push_back({u, id});
return id;
}
void emit_block(int stop_edge) {
++mark_id;
vector<int> vertices;
while (true) {
int id = edge_stack.back();
edge_stack.pop_back();
auto [u, v] = edges[id];
if (mark[u] != mark_id) {
mark[u] = mark_id;
vertices.push_back(u);
}
if (mark[v] != mark_id) {
mark[v] = mark_id;
vertices.push_back(v);
}
if (id == stop_edge) break;
}
blocks.push_back(move(vertices));
}
void dfs(int u, int parent_edge) {
dfn[u] = low[u] = ++timer;
int child_count = 0;
for (auto [v, id] : g[u]) {
if (id == parent_edge) continue;
if (!dfn[v]) {
++child_count;
edge_stack.push_back(id);
dfs(v, id);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
if (parent_edge != -1 || child_count >= 2) {
is_cut[u] = true;
}
emit_block(id);
}
} else if (dfn[v] < dfn[u]) {
// 每条返祖边只在较深端压栈一次。
edge_stack.push_back(id);
low[u] = min(low[u], dfn[v]);
}
}
// 根节点是否为割点由 DFS 树儿子数单独判定。
if (parent_edge == -1) {
is_cut[u] = (child_count >= 2);
}
}
void run() {
for (int u = 0; u < n; ++u) {
if (dfn[u]) continue;
if (g[u].empty()) {
dfn[u] = low[u] = ++timer;
blocks.push_back({u});
} else {
dfs(u, -1);
}
}
}
// 建立圆方树:原圖頂點為 0..n-1,点双分量节点为 n..n+B-1。
// 原顶点与包含它的每个点双分量相连。结果是森林。
vector<vector<int>> build_block_cut_forest() const {
int B = (int)blocks.size();
vector<vector<int>> forest(n + B);
for (int b = 0; b < B; ++b) {
int block_node = n + b;
for (int v : blocks[b]) {
forest[v].push_back(block_node);
forest[block_node].push_back(v);
}
}
return forest;
}
};
调用 run() 后:is_cut[u] 表示割点,blocks 保存每个点双分量的顶点集合。若只需要割点,不需要边栈和 blocks,可以写更轻量的 DFS。
圆方树也常称 block-cut forest:原点是圆点、点双分量是方点。对连通图它是一棵树;对断开图它是森林。割点会连接多个方点,非割点通常只连接一个方点。利用它可处理“删除某点后是否断开”“两点间经过哪些割点”等路径问题。
4. 三类结果的快速区分
| 目标 | 图类型 | 核心判定/定义 | 典型缩点结构 |
|---|---|---|---|
| 强连通分量 SCC | 有向图 | 分量内任意两点互相可达 | DAG |
| 边双连通分量 | 无向图 | 任意两点间有两条边不相交路径;分量间分隔边是桥 | 桥森林 |
| 点双连通分量 | 无向图 | 分量内任意两点间有两条内部点不相交路径(常用 block 定义) | 圆方树 / block-cut forest |
“双连通”一词在不同题目里可能指边双或点双。读题时确认是不共边还是不共点;算法条件分别是桥与割点,不能混用。
5. 比赛检查清单
- 无向图跳过父边时用边编号,保证平行边正确。
- SCC 的已访问边只有指向
in_stack点时才能更新low。 - 无向图只把返祖边(指向祖先)压入点双边栈一次。
- 桥条件是
low[child] > dfn[parent];割点非根条件是low[child] >= dfn[parent]。 - DFS 根的割点条件单独按树儿子数至少 \(2\) 判断。
- 断开图要从所有未访问点启动 DFS。
- 深度可能达到 \(10^5\) 或更多时,评估递归栈限制;必要时改迭代版。
- 缩点后若题目要求简单 DAG/简单森林,记得去重;多数 DP 不应默认输入没有平行边。
- 点双模板按常见 block 定义处理孤立点为单点分量;自环通常需要按题目定义单独处理,示例模板假设无自环。

浙公网安备 33010602011771号