【笔记】Tarjan算法
dfs生成树
对一张图进行 dfs,我们会得到包含一大堆非树边的“树”。
这棵树上除了正常的树边(父→子),还有:
- 返祖边:\(u \to v\ (v \in {\rm Anc}(u))\) ;
- 前向边:\(u \to v\ (v \in {\rm Subtree}(u))\) ;
- 横叉边:\(u \to v\ ({\rm vis}(v) = {\rm true} \land v \notin {\rm Anc}(u) \cup {\rm \rm Subtree}(u))\) .
在dfs的过程中,Tarjan算法维护了这样两个变量数组: - \({\rm dfn}_u\):dfs 序(先序);
- \({\rm low}_u\):\(u\) 的子树中能回溯到的最早的节点,形式化地,\({\rm low}_u = \min\limits_{v \in {\rm Subtree}(u) \cup {\rm To}(u)}\{{\rm dfn}_v\}\) .
强连通分量(Strongly Connected Components)
\(\rm Lm.\) 如果节点 \(u\) 是某个强连通分量在搜索树上的第一个结点(\(\rm dfn\) 序最小的),则这个强连通分量在搜索树以 \(u\) 为根的子树中。
证:因为 \(\rm dfn\) 序最小嘛……
\(\rm Th.\) 如果 \({\rm dfn}_u = {\rm low}_u\),则 \(u\) 和栈中在 \(u\) 上方的节点构成一个强连通分量。(这就是上面引理中的那“第一个”节点)
\(\rm E.g.\) B3609 [图论与代数结构 701] 强连通分量
void dfs(int u) {
dfn[u] = low[u] = ++dfs_cnt;
in_stk[stk[++top] = u] = true;
for(int v : to[u]) {
if(!dfn[v]) { //树边
dfs(v);
low[u] = min(low[u],low[v]);
} else if(in_stk[v]) //非树边
low[u] = min(low[u],dfn[v]);
}
if(dfn[u] == low[u]) {
++sc;
do {
blg[stk[top]] = sc;
++sz[sc];
in_stk[stk[top]] = false;
} while(stk[top--] != u);
}
}
割点与桥
P3388 【模板】割点(割顶)
\(\rm Th.\) 如果对于一个节点 \(u\),它的一个子节点 \(v\) 不能回溯到之前的节点,则 \(u\) 为割点。形式化地,\({\rm low}_v \ge {\rm dfn}_u\) 。
void dfs(int u,int fa) {
dfn[u] = low[u] = ++dfs_cnt;
int ch_cnt = 0;
for(int v : to[u]) {
if(!dfn[v]) {
++ch_cnt;
dfs(v,u);
low[u] = min(low[u],low[v]);
if(low[v] >= dfn[u]&&!cutv[u]) {
cutv[u] = true;
++cutv_tot;
}
} else if(fa != v)
low[u] = min(low[u],dfn[v]);
}
if(fa == u&&ch_cnt <= 1&&cutv[u]) {
cutv[u] = false;
--cutv_tot;
}
}
值得注意的是,对于 dfs 的起点,只有一个子节点时必不为割点,除去这种情况。
\(\rm Th.\) 在一个简单图中,对于一个节点 \(u\) 及其搜索树中的子节点 \(v\),若 \({\rm low}_v > {\rm dfn}_u\),则边 \(\{u,v\}\) 为割边。
这个跟割点几乎就是一样的,还不用考虑根节点(搜索起点)
边双连通分量
P8436 【模板】边双连通分量
\(\rm Th.\) 把无向边拆成两个有向边,新的有向图中的强连通分量是原无向图中的边双连通分量。
void dfs(int u,int fa) {
dfn[u] = low[u] = ++dfs_cnt;
in_stk[stk[++top] = u] = true;
bool to_fa = false;
for(int v : to[u]) {
if(v == fa&&!to_fa) {
to_fa = true;
continue;
}
if(!dfn[v]) {
dfs(v,u);
low[u] = min(low[u],low[v]);
} else if(in_stk[v])
low[u] = min(low[u],dfn[v]);
}
if(dfn[u] == low[u]) {
++sc;
do {
blg[stk[top]] = sc;
++sz[sc];
in_stk[stk[top]] = false;
} while(stk[top--] != u);
}
}
注意到:布尔变量 to_fa,我们排除掉了去往父节点的边(因为本来是无向图,这俩是一条边),但是还要考虑重边的存在。
当然,我们还有一种选择,就是去掉所有桥后找连通分量。
点双连通分量
P8435 【模板】点双连通分量
\(\rm Th.\) 两个点双连通分量的分界是割点,割点属于两个点双连通分量。
void dfs(int u,int fa) {
dfn[u] = low[u] = ++dfs_cnt;
stk[++top] = u;
if(u == fa&&to[u].size() == 0) {
vcc[++vcc_cnt].push_back(u);
return;
}
int ch_cnt = 0;
for(int v : to[u]) {
if(!dfn[v]) {
dfs(v,u);
low[u] = min(low[u],low[v]);
if(low[v] >= dfn[u]) {
if(++ch_cnt > 1||u != fa)
cutv[u] = true;
++vcc_cnt;
do vcc[vcc_cnt].push_back(stk[top--]);
while(stk[top+1] != v);
vcc[vcc_cnt].push_back(u);
}
} else
low[u] = min(low[u],dfn[v]);
}
}
如果搜索树只有一个结点的话,这个节点单成一个点双。

浙公网安备 33010602011771号