Tarjan 求强连通分量

Tarjan 求强连通分量

前置知识

DFS 序

就是通过 DFS 前序遍历遍历到的次序,节点 \(u\) 的 DFS 序记作 \(dfn_u\)。

cpp 复制代码
int dfn[N], T = 0;
void tarjan(int u) {
    dfn[u] = ++T;
}

DFS 树

就是通过 DFS 遍历遍历的边。

如图,红色为树边,绿色为前向边(因为不影响连通性,可以忽略),黄色为返祖边,蓝色为横叉边。

我们可以注意到横叉边只能从右到左(这里的左右代表子树遍历顺序,规定从左到右就是子树遍历的顺序),因为从左到右的边会被当成树边。

算法步骤

我们规定 \(low_u\) 是节点 \(u\) 在同一 SCC 内至多通过一条非树边 能到达的最小的 DFS 序。当然,\(low_u \le dfn_u\) 。

在遍历图的时候,我们可以维护一个栈,用于存储可以组成 SCC 的节点。在每一个节点访问到时,就把它压进栈中。我们可以轻松的得到一个性质:当一个节点 \(u\) 在回溯的时候,在栈里在 \(u\) 上面的节点都在 \(u\) 的子树内。

我们注意到如果 \(low_u=dfn_u\) ,代表整个子树都无法通过返祖边和前向边跳上去。也就是说,在栈中属于 \(u\) 的子树的节点,就构成了一个 SCC。接着我们只要不断弹栈直到弹出 \(u\) 即可。

注意:在 \(u\) 的子树内如果不与其他构成 SCC 的节点,在回溯到 \(u\) 之前就已经被弹栈了。

接着我们考虑如何更新 \(low_u\)。我们注意到,只要 \(u\) 的子树内能跳到的节点,\(u\) 一定能跳到。所以在遍历子节点的时候,\(low_u\gets\min\limits_{v\in\{u\text{ 的所有树边的终点}\}}\{low_v\}\)。当遍历到的是横叉边或返祖边时,先判断是否在栈中。如果在栈中,就用 \(dfn_v\) 去更新,否则就代表不在同一 SCC 内。

代码:

cpp 复制代码
void tarjan(int u) {
    dfn[u] = low[u] = ++T, stk[++top] = u, ins[u] = 1;
    for(int x = beg[u], i = to[x]; x; x = nxt[x], i = to[x]) {
        if(!dfn[i]) dfs(i), low[u] = min(low[u], low[i]);
        else if(ins[i]) low[u] = min(low[u], low[i]);
    }
    if(dfn[u] == low[u]) {
        ++C;
        int tot = 0;
        while(stk[top] != u)
            S[stk[top]] = C, ins[stk[top]] = 0, tot += val[stk[top--]];
        ins[u] = 0, S[u] = C, tot += val[u], --top;
        val1[C] = tot;
    }
}

缩点(补充)

我们遍历所有边,对于一条边 \(u \to v\),如果 \(u\) 和 \(v\) 所属的 SCC 不相同,就在新图中建边。

代码:

cpp 复制代码
for(int u = 1; u <= n; u++)
        for(int i = beg[u], v = to[i]; i; i = nxt[i], v = to[i])
            if(S[u] != S[v]) add1(S[u], S[v]);