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

定义

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

Tarjan 的做法

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

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

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

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

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

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

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

一个结点的子树内结点的 dfn 都大于该结点的 dfn

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

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

  1. v 未被访问过。继续对 v 进行 DFS ,在回溯的过程中用 low(v) 更新 low(u) 。理由是:如果 v 能回到比 u 更早的结点,那 u 一定能通过走 uv 这条路径走到更早的结点
  2. v 被访问过,且还在栈中,那么就用 dfn(v) 更新 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 缩点,然后跑一个最长路即可。

posted @   SD!LTF  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
点击右上角即可分享
微信分享提示