dfn 是 Depth-First Number,记录首次访问顺序;令 low[u]=dfn[u]。
强连通分量 SCC
Tarjan 工作原理
把“已经访问但归属未定”的点留在栈里,用 low 判断这片区域何时真正封口。
核心判据
dfn[u] == low[u] ⇒ 从栈顶弹到 u一、一句话看懂 Tarjan
DFS 负责向前探索,dfn 记录到达顺序,low 记录最早能追回的未封口点,栈保存归属待定的点;追不到更早处时,整段一起弹出成为 SCC。
把 u 入栈,因为它的 SCC 还没有确定。
沿树边继承 low[v];遇栈内点使用 dfn[v]。
dfn[u]==low[u] 时,从栈顶弹到 u。
建模对象有向图的互相可达区域
算法范式DFS + 连通性分解
维护信息dfn、low、栈、inStack
压缩信息不保存所有可达路径
易卡点两种 low 更新与弹栈边界
二、先分清 SCC、环与连通块
它们都在说“连在一起”,但环看路线,SCC 看有向互达分组,连通块看无向连接。
环
1 → 2 → 3 → 1
沿箭头走一圈回到起点。环是一条闭合路线,不要求包含尽可能多的点。
强连通分量 SCC
任意 u、v:u ⇝ v 且 v ⇝ u
集合内任意两点都能互相到达,并且不能再加入其他点。所有原点都会被分入某个 SCC。
连通块
把 → 暂时看成 —
通常用于无向图。有向图忽略方向后得到的分组,更准确地叫弱连通分量。
同一张 6 点图,三个问题有三个答案
有哪些环?
1→2→3→14→5→4有哪些 SCC?
{1,2,3}、{4,5}、{6}忽略方向有几个连通块?只有 1 个:
{1,2,3,4,5,6}辨认口令:环看闭合路线;SCC 看两两互达的极大点集;连通块看忽略方向后是否连成一片。
有向环上的点一定在同一 SCC
一个 SCC 可以包含多个环
一个弱连通分量可以包含多个 SCC
点 6 无环也能自成 SCC
三、跟着状态单步运行
看红色当前边、粗黄环当前点、右侧执行规则和下方 dfn/low 状态一起变化。
先认识这张图
步骤 0 / 10① 编号入栈
② DFS 探索
③ low 追溯
④ SCC 封口
未访问
栈内待定
当前点
当前边
已归入 SCC
四、为什么等号就是封口线
还不能弹
low[u] < dfn[u]
更早点u子树点
u 所在区域还能追回左侧更早的未封口点。现在弹栈,会把本应属于同一个 SCC 的点拆开。
现在可以弹
low[u] == dfn[u]
u · 根同 SCC栈顶
这片区域追不到比 u 更早的栈内点,边界已经确定。从栈顶弹到 u,刚好得到一个极大的 SCC。
五、动画对应到四处代码
读实现时只抓状态的“建立、传播、回边更新、封口”四个位置。
tarjan.cpp · 核心函数
void tarjan(int u) {
// ① 建立状态:dfn 固定,low 从自己开始
dfn[u] = low[u] = ++timer;
nodeStack.push(u);
inStack[u] = true;
for (int v : graph[u]) {
if (!dfn[v]) {
tarjan(v);
// ② 树边:递归返回后继承子树的追溯能力
low[u] = min(low[u], low[v]);
} else if (inStack[v]) {
// ③ 直接连到栈内点 v:使用 dfn[v]
low[u] = min(low[u], dfn[v]);
}
}
// ④ u 是当前 SCC 最早访问的点
if (dfn[u] == low[u]) {
++componentCount;
while (true) {
int x = nodeStack.top();
nodeStack.pop();
inStack[x] = false;
component[x] = componentCount;
if (x == u) break;
}
}
}
① 入栈
已访问,但 SCC 归属还没确定。
已访问,但 SCC 归属还没确定。
② 树边
通过子树到达,所以继承
通过子树到达,所以继承
low[v]。③ 栈内边
直接到达点
直接到达点
v,所以使用 dfn[v]。④ 弹栈
从栈顶一直弹到根
从栈顶一直弹到根
u,不能只弹一个点。六、Tarjan 的一个应用:缩点
先用 Tarjan 给每个原点贴上 SCC 编号,再把原边的两个端点换成 SCC 编号。
本质:把每个 SCC 当成一个超级点。分量内部的绕行细节被压掉,分量之间的方向关系被保留下来。
原图:环藏在三个 SCC 中
comp[u] == comp[v]内部边消失
→
comp[u] != comp[v]保留 comp[u]→comp[v]
新图:三个超级点组成 DAG
1. 分组Tarjan 求出每个点的
component[u];编号大小无意义,只看是否相同。2. 换点建边重扫原边,只保留端点属于不同 SCC 的边。
3. 合并信息若有点权,把同一 SCC 内的点权累加到超级点。
for (auto [u,v] : edges) {
if (component[u] != component[v])
dag[component[u]].push_back(component[v]);
}
为什么一定是 DAG? 如果缩点后仍有分量环,环上的各分量就能互相到达,本应被 Tarjan 合并成同一个 SCC,产生矛盾。
七、三问验收
1. 栈中保存的是什么?
选择后显示解释。
2. 看到栈内边 u→v,更新什么?
选择后显示解释。
3. 什么时候封口?
选择后显示解释。
复杂度
O(n+m):每个点入栈、出栈一次,每条边检查一次。最常见错误
忽略
忽略
inStack[v]、栈内边误用 low[v]、只弹出根节点。