Tarjan学习笔记

众所周知,Tarjan 可以用来求有向图的强连通分量,我们就不扯那些 dfs 生成树,前向边、返祖边之类的东西,直接步入正题。

准备工作

Tarjan 算法本质上是一次 dfs 的过程,我们用 dfn[u]dfn[u] 记录 uu 结点被 dfs 到的顺序,用 low[u]low[u] 记录 uu 能到达的所有结点中最小的 dfndfn(包括自己)(详细定义:能够回溯到的最早的已经在栈中的结点。设以 uu 为根的子树为 subtreeusubtree_u,则 low[u]low[u] 定义为以下结点的 dfndfn 的最小值:subtreeusubtree_u 中的结点;从 subtreeusubtree_u 通过一条不在搜索树上的边能到达的结点。)。dfndfn 很好维护,dfs 的过程中记录一下即可。接下来为重头戏:low[u]low[u] 的维护与强联通分量的计算。

正式开始

考虑从一个结点 uu 到另一个结点 vv 的过程中会发生什么。

  1. 如果 vv 在栈中(即那个节点还没计算完),那么 vv 一定是 uu 的祖先,则用 dfn[v]\mathbf{dfn}[v] 更新 low[u]low[u]
  2. 如果 vv 访问过且不在栈中(即计算完了),那么装作无事发生。
  3. 如果 vv 还没被访问过,那么递归访问,用 low[v]\mathbf{low}[v] 更新 low[u]low[u]

uu 能直接到达的所有 vv 都算完后,就可以计算强连通分量了。我们发现,如果一个结点算完后 low[u]=dfn[u]low[u]=dfn[u],则说明:这个结点是当前强联通分量中最先到达的(即 dfndfn 最小的,称为这个强连通分量的根)

在这个图中,22 号节点就是强连通分量 {2,3,4,5,6,7}\{2,3,4,5,6,7\} 中的根。

这时候,我们就可以处理 uu 的强连通分量了!循环让结点出栈,直到栈顶元素为 uu 自己(因为 uu 是强连通分量的根,所以它一定是该强连通分量在栈中最靠前的),这些节点都在同一个以 uu 为根的强连通分量里。

如果一个结点算完后 low[u]dfn[u]low[u]\ne dfn[u],则意味着它能到达一个它的祖先,我们暂时先不处理它,也不出栈(还没算完强连通分量怎么能出栈呢),直接返回,到他的那个强连通分量的根的时候再去算强连通分量。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//num记录当前一共遍历了几个结点,用于计算新结点的dfn
//col[i]记录第节点i在哪个强连通分量里,root[i]表示i所在强连通分量的根
//colnum记录当前一共算完了几个强连通分量,用来计算新强连通分量的编号
int tarjan(int now) {
s.push(now),vis[now]=1;
low[now]=dfn[now]=++num;
for(int i=head[now];i;i=e[i].next)
if(!dfn[e[i].to])
low[now]=min(low[now],tarjan(e[i].to));
else if(vis[e[i].to])
low[now]=min(low[now],dfn[e[i].to]);
if(dfn[now]==low[now]) {
vis[now]=0;
col[now]=++colnum,root[now]=now;
while(s.top()!=now) {
col[s.top()]=colnum,root[s.top()]=now;
vis[s.top()]=0,s.pop();
}
s.pop();
}
return low[now];
}

扩展阅读

建议参考阅读这篇文章的手动模拟算法的部分,会有更直观的理解。

有兴趣看看更严谨的说明,请上 oi-wiki 观看。