强连通分量
时间戳 dfn[x]:节点 x 第一次被访问的顺序
回溯值 low[x]:从节点 x 出发所能访问的最早时间戳
时间复杂度 O(n+m)
c++
vector<int> g[N];
int dfn[N], low[N], tot;
int stk[N], instk[N], top;
int scc[N], sz[N], cnt;
void tarjan(int x) {
dfn[x] = low[x] = ++ tot;
stk[++ top] = x, instk[x] = 1;
for (int y: g[x]) {
if (!dfn[y]) {
tarjan(y);
low[x] = min(low[x], low[y]);
}
else if (instk[y])
low[x] = min(low[x], dfn[y]);
}
if (dfn[x] == low[x]) {
++ cnt;
while (stk[top] != x) {
scc[stk[top]] = cnt;
sz[cnt] ++;
instk[stk[top]] = 0;
top --;
}
scc[x] = cnt;
sz[cnt] ++;
instk[x] = 0;
top --;
}
}
割点
判定规则:
如果 x 不是根节点,当搜索树中存在 x 的子节点 y,满足 low[y] >= dfn[x],那么 x 是割点
如果 x 是根节点,当搜索树至少存在两个子节点 y1, y2,满足上述条件,那么 x 是割点
证明:从 y 出发,在不通过 x 的前提下,不管走到哪都无法到达比 x 更早访问的节点,故删去 x 后以 y 为根的子树断开
如果有重边和自环,不影响判定
c++
vector<int> g[N];
int dfn[N], low[N], tot;
int cut[N], root;
void tarjan(int x) {
dfn[x] = low[x] = ++ tot;
int child = 0;
for (int y: g[x]) {
if (!dfn[y]) {
tarjan(y);
low[x] = min(low[x], low[y]);
if (low[y] >= dfn[x]) {
child ++;
if (x != root || child > 1) cut[x] = 1;
}
}
else low[x] = min(low[x], dfn[y]);
}
}
割边
判定规则:满足 low[y] > dfn[x] 即可,(x, y) 就是割边
为什么不加等号:因为判定割边时,不允许走 (x, y) 的反边更新 low 值
下列代码中,当 isBridge[x] = 1 时,(fa[x], x) 为割边
c++
vector<int> g[N];
int dfn[N], low[N], fa[N], tot, cnt;
bool isBridge[N];
void tarjan(int u, int f) {
fa[u] = f;
dfn[u] = low[u] = ++ tot;
for (int v: g[u]) {
if (!dfn[v]) {
tarjan(v, u);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u]) {
isBridge[v] = 1;
++ cnt;
}
}
else if (dfn[v] < dfn[u] && v != fa)
low[u] = min(low[u], dfn[v]);
}
}