图论·可视化实验室

Tarjan SCC
看见“封口”的瞬间

每点一次“下一步”,同时观察图、dfn/low、未封口栈与已弹出的强连通分量。

一、先把结论钉住

弹栈不是因为 DFS “走完了”,而是因为当前区域已经无法追到更早的未封口点。

1u 能到达上方的点

这些点是从 u 沿 DFS 树边访问到的。

2留在栈中意味着还未封口

若某一支不能回到这片区域,它会更早单独弹出。

3low[u] == dfn[u]

整片区域无法追到比 u 更早的栈内点,边界已确定。

所以,从栈顶一直弹到 u,得到的正好是一个极大的 SCC。

二、单步跑一遍 Tarjan

固定从 1 开始 DFS,边的检查顺序为 3→1 先于 3→45→4 先于 5→6

准备开始

步骤 0 / 14
?未访问 栈内待定 当前点(黄环) 当前边 S已归入 SCC
Tarjan SCC 单步图 图包含 1 到 6 号点,动态显示当前边、栈内点和已弹出的强连通分量。 1→2 2→3 3→1(回边) 3→4 4→5 5→4 5→6 1dfn=— low=— 2dfn=— low=— 3dfn=— low=— 4dfn=— low=— 5dfn=— low=— 6dfn=— low=—
dfnlow状态
1未访问
2未访问
3未访问
4未访问
5未访问
6未访问

三、为什么等号就是“封口线”

还不能弹:
low[u] < dfn[u]
更早的点u子树点
  • 当前区域还能追到 u 之前的栈内点。
  • 现在弹栈会把本应同属一个 SCC 的点拆开。
可以封口:
low[u] == dfn[u]
u(根)同 SCC栈顶
  • 上方未封口点能回到 u 所在区域。
  • 整体又无法追到比 u 更早的点,边界已经极大。

四、对应到关键代码

读代码只抓三件事:初次入栈、两种 low 更新、根节点弹栈。

tarjan.cpp
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;
        }
    }
}

五、三个最容易错的地方

回边写成 low[v]

直接连到的是 v,此时用 dfn[v]

连到已出栈点仍更新

已出栈点属于其他 SCC,不再影响当前 low

只弹出根 u

必须从栈顶一直弹到 u,这整段才是一个 SCC。