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]);