tarjan 算法の学习笔记
前言
Tarjan 算法其实分很多类,比如常见的就有两类:
- tarjan 强连通分量算法。
- tarjan 求割点割边(点双和边双)。
这两类算法虽然实现细节不同,但是 tarjan 算法的思路与大体代码结构都是相同的,所以一定要学懂。
本博客分几个内容:一部分是讲解算法,另一部分是讲解经典题目。
Tarjan 求强连通分量
首先我们要明白强连通分量是啥:
-
强连通:在一张有向图中,\(u \to v \wedge v \to u\),则称点 \(u\) 和点 \(v\) 是强连通的。
-
有向图的一张极大子图 \(G\),\(G\) 中任意两点都是强连通的,则称 \(G\) 是一个强连通分量。
还有一些特殊情况的强连通分量:
- 单点算一个强连通分量。
- 有向图最少只有一个强连通分量,最多有 \(n\) 个。
- 不难发现,强连通分量要么是一个点,要不是一个环(可能是复杂环)。
然后我们在题目中经常整个强连通分量的答案或者贡献可以直接算出,且问题在有向无环图(DAG)中容易求出,此时我们就可以用 tarjan 算法求出所有的强连通分量,将每个强连通分量缩成一个点,不难发现缩点之后的图是一个 DAG,此时可以方便的求解问题。
然后我们学习 tarjan 算法是如何求强连通分量的,首先我们需要知道 tarjan 算法是如何工作的,tarjan 算法的基础是建立一棵 dfs 树,在 dfs 树上求解问题。我们需要了解两个数组:
-
\(\text{dfn}_x\),表示点 \(x\) 在 dfs 树中被搜索到的时间,也可以理解为 dfs 序,通常称为时间戳。
-
\(\text{low}_x\),表示点 \(x\) 在 dfs 树中通过 \(x\) 连出去的边能够到达的最小的 \(\text{dfn}\) 值,因为我们的 dfs 树上还有其他的边。
然后 tarjan 算法的原理也特别简单:
- 如果访问到一个点,那么入栈。
- 如果当前的 \(\text{low}_x\) 更新完成后,如果与 \(\text{dfn}_x\) 相等,那么已经有了一个强连通分量,一直出栈到 \(x\),这些点构成一个强连通分量。
那么我们分析为什么会这样,因为显然我们的 \(\text{low}\) 只会比 \(\text{dfn}\) 相等甚至是更小,所以只可能在这个点上头,那么强连通分量的开始只可能在上头,如果相等的话,那么说明下面走走走走到了这个点本身,很显然是一个环,即使是一个复杂环(其实更标准的说法是下面有返祖边和横叉边)。
那么我们考虑如何求出 \(\text{low}\)(\(\text{dfn}\) 是好求的):
- 如果一个连出去点未被访问过,那么访问它,并与它的 \(\text{low}\) 取 \(\min\)。
- 如果一个点访问过且在栈里,那么与它的 \(\text{dfn}\) 或者 \(\text{low}\) 取 \(\min\) 即可。
为什么第二步看起来这么粗糙呢?容易证明 \(\text{low}\) 与 \(\text{dfn}\) 是相等的,如果不相等,已经出栈了(边双中可以写 \(\text{low}\),但是点双中只能写 \(\text{dfn}\))。
然后 tarjan 算法实现的思路就讲完了。
PS:注意到 tarjan 的强连通分量的编号是反拓扑序的,所以这个性质在 2-SAT 中有大用。
代码上来:
void tarjan ( int x ) {
dfn[x] = low[x] = ++ idx; // 初始为自身
vis[x] = true; // vis 表示在没在栈里
stk.push ( x );
for ( int i = head[x]; i; i = edges[i].next ) {
if ( !dfn[edges[i].to] ) {
tarjan ( edges[i].to );
low[x] = min ( low[x], low[edges[i].to] );
}
else if ( vis[edges[i].to] ) { // 访问过且在栈里
low[x] = min ( low[x], dfn[edges[i].to] );
}
}
if ( dfn[x] == low[x] ) { // 找到一个强连通分量
int tmp;
do {
tmp = stk.top ();
stk.pop ();
vis[tmp] = false;
} while ( tmp != x ); // 这里使用 do-while
}
}

浙公网安备 33010602011771号