强连通分量Tarjan算法学习笔记

发布时间 2023-08-07 15:24:00作者: SD!LTF

定义

一个有向图 \(G\) 强连通,指的是 \(G\) 的任意两个结点连通。强连通分量 SCC 指的是极大的强连通子图。

Tarjan 的做法

首先来看一个 DFS 树,图源 OI Wiki

其中所有的边都是原有向图的边。从 \(1\) 号节点开始跑 DFS ,我们把这些边分为两类:

  • 树边,也就是图中黑色的边,构成了 DFS 树本树;
  • 返祖边,也就是图中红色的边,返祖边的出现可能会带来环;
  • 横叉边,也就是图中红色的边,在 DFS 树中,在同一层之间连接点的边即为横叉边;
  • 前向边,也就是途中绿色的边,在 DFS 树种主打一个“跳级”操作。

那么,你可以思考到,如果节点 \(u\) 是某个强连通分量在搜索树种遇到的第一个节点,那么其余结点一定在以 \(u\) 为根的子树中。

反证法:假设有个结点 \(v\) 在该强连通分量中但是不在以 \(u\) 为根的子树中,那么 \(u\)\(v\) 的路径中肯定有一条离开子树的边。但是这样的边只可能是横叉边或者反祖边,然而这两条边都要求指向的结点已经被访问过了,这就和 \(u\) 是第一个访问的结点矛盾了。得证。

Tarjan 还另外对每个维护了两个变量:

  • \(\operatorname{dfn}(u)\):深度优先搜索时,结点 \(u\) 被搜到的次序;
  • \(\operatorname{low}(u)\):从 \(u\) 开始搜索,能搜索到的深度最低的结点 —— 显然,如果 \(u\) 上有一个返祖边,肯定是跟着返祖边往上跑;再就是寄希望于自己的孩子能有一个返祖边往回跑,否则就只能是他自身。

一个结点的子树内结点的 \(\operatorname{dfn}\) 都大于该结点的 \(\operatorname{dfn}\)

并且可以观察到:从根开始的一条路径上的 \(\operatorname{dfn}\) 严格递增,\(\operatorname{low}\) 严格非降。

那么接下来就是 Tarjan 算法的流程:
按照深度优先搜索的顺序对所有的结点进行搜索,维护每个节点的 \(\operatorname{dfn}\)\(\operatorname{low}\) ,并让这个结点入栈。
在搜索过程中,对于 \(u\) 与其相邻的非父结点 \(v\) ,只可能有以下三种情况

  1. \(v\) 未被访问过。继续对 \(v\) 进行 DFS ,在回溯的过程中用 \(\operatorname{low}(v)\) 更新 \(\operatorname{low}(u)\) 。理由是:如果 \(v\) 能回到比 \(u\) 更早的结点,那 \(u\) 一定能通过走 \(u\rightarrow v\) 这条路径走到更早的结点
  2. \(v\) 被访问过,且还在栈中,那么就用 \(\operatorname{dfn}(v)\) 更新 \(\operatorname{low}(u)\)
  3. \(v\) 被访问过,已不在栈中:说明 \(v\) 已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。

画图理解如下:







实现如下

void tarjan(int u) {
	low[u] = dfn[u] = ++dfncnt, s[++tp] = u, in_stack[u] = 1;
	for (int i = h[u]; i; i = e[i].nex) {
	    const int &v = e[i].t;
	    if (!dfn[v]) {
			tarjan(v);
	    	low[u] = min(low[u], low[v]);
	    } 
		else if (in_stack[v]) low[u] = min(low[u], dfn[v]); // 回溯 
	}
	if (dfn[u] == low[u]) {
	    ++ sc;
	    while (s[tp] != u) {
	    	scc[s[tp]] = sc;
	    	++ sz[sc];
	    	in_stack[s[tp]] = 0;
	    	--tp;
	    }
	    scc[s[tp]] = sc;
	    ++ sz[sc];
	    in_stack[s[tp]] = 0;
	    -- tp;
	}
}

应用

把一个图的强连通分量找出来,可以把它缩成一个点。

于是这张图就变成了一个 DAG 。

缩点的一个经典应用如下

求一条路径,可以经过重复结点,要求经过的不同结点数量最多。

容易想到,如果我们能走一个环,那走一个环一定很优,可以使用 Tarjan 缩点,然后跑一个最长路即可。