这些点是从 u 沿 DFS 树边访问到的。
Tarjan SCC
看见“封口”的瞬间
每点一次“下一步”,同时观察图、dfn/low、未封口栈与已弹出的强连通分量。
一、先把结论钉住
弹栈不是因为 DFS “走完了”,而是因为当前区域已经无法追到更早的未封口点。
若某一支不能回到这片区域,它会更早单独弹出。
整片区域无法追到比 u 更早的栈内点,边界已确定。
所以,从栈顶一直弹到
u,得到的正好是一个极大的 SCC。二、单步跑一遍 Tarjan
固定从 1 开始 DFS,边的检查顺序为 3→1 先于 3→4,5→4 先于 5→6。
准备开始
步骤 0 / 14
?未访问
栈栈内待定
●当前点(黄环)
当前边
S已归入 SCC
| 点 | dfn | low | 状态 |
|---|---|---|---|
| 1 | — | — | 未访问 |
| 2 | — | — | 未访问 |
| 3 | — | — | 未访问 |
| 4 | — | — | 未访问 |
| 5 | — | — | 未访问 |
| 6 | — | — | 未访问 |
三、为什么等号就是“封口线”
low[u] < dfn[u]
更早的点u子树点
- 当前区域还能追到
u之前的栈内点。 - 现在弹栈会把本应同属一个 SCC 的点拆开。
low[u] == dfn[u]
u(根)同 SCC栈顶
- 上方未封口点能回到
u所在区域。 - 整体又无法追到比
u更早的点,边界已经极大。
四、对应到关键代码
读代码只抓三件事:初次入栈、两种 low 更新、根节点弹栈。
void tarjan(int u) {
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]) {
low[u] = min(low[u], dfn[v]); // 直接连到栈内点 v
}
}
if (dfn[u] == low[u]) { // u 是 SCC 的根
componentCount++;
while (true) {
int x = nodeStack.top();
nodeStack.pop();
inStack[x] = false;
component[x] = componentCount;
if (x == u) break;
}
}
}
五、三个最容易错的地方
直接连到的是 v,此时用 dfn[v]。
已出栈点属于其他 SCC,不再影响当前 low。
必须从栈顶一直弹到 u,这整段才是一个 SCC。