强联通分量 StronglyConnectedComponents,SCC
什么是强联通分量
-
联通分量: 有向图中, 对一个联通分量中的任意两点u,v(u可能等于v), 一定存在u→v和
v→u的路径. -
强联通分量: “极大”联通分量 — 如果在某个联通分量中加入任意不在该联通分量的一个顶点后,
该分量不再联通, 则称这个联通分量为强联通分量.
SCC的应用
对于任意一个强联通分量, 对于其中任意两点u,v存在路径u→v与v→u, 存在至少一个环.
而强联通分量之间不存在环, 否则可以合成一个联通分量. 因此如果我们对每个强连通分量进行
缩点操作, 则新图一定是一个有向无环图(DAG, 也称拓扑图).
有向图拥有更简单的结构, 对某些问题可以用相对简单的算法求解.
缩点操作: 同一强联通分量内的顶点视为一个顶点, 只保留不同强联通分量之间的边.
Tarjan求强联通分量
dfs中的4类边
如果我们在图中按dfs的顺序建立一颗树(深度搜索树), 这颗树存在4类边:
-
树枝边: 树(深度搜索树)边.
-
前向边: u指向自己子孙v的边. 树枝边可以认为是前向边的一种特殊情况.
-
后向边: v指向自己的祖先u的边, 是图中存在联通分量的关键.
-
横叉边: u指向左边分支节点v. 不存在指向右边, 否则其顺序恰好是dfs遍历顺序, 即树枝边.
Tarjan算法思想
在有向图中如何形成一个联通分量:
-
顶点u存在后向边.
-
顶点u存在横插边指向v, 而v的后续边指向u的祖先节点.
Tarjan算法具体实现
引入时间戳概念, 对每个顶点记录两个时间戳:
-
dfn(u): dfs执行过程中, 顶点u被访问的时间.
-
low(u): 顶点u经过后续节点可以到达的最小时间点.
此外通过栈保存: 已经被dfs访问且还未找到对应SCC的顶点.
判断SCC: 对于顶点u, 若dfn(u)==low(u), 则从栈顶到u的顶点均属于同一SCC.
-
dfn(u)==low(u)可以理解为从u出发经过某些环最后到达自身, 且不能到达其他祖先节点.
-
从栈顶到u均属于同一SCC: 假设存在顶点v在栈中且不属于当前SCC, 设顶点v对应强联通分量
代表节点为v′(即dfn(v′)==low(v′)), 则dfs顺序只能是u→v′→v, 算法保证
当回溯至v′时, 会将v出栈, 所以假设不成立.
一个简单例子
假设原图如下所示:
tarjan算法基于dfs, 访问顺序为a→b→c→d, 对应(dfn,low)与栈内元素如下图:
当算法到达d时, 由于d存在指向a的后向边且此时a仍在栈中, 所以low(d)可更新为2
.
对于c节点, 其到达的子节点d可以到达的最小时间戳为2
, 所以low(c)可更新为2
. b同理.
对于b节点, 其不存在可以到达更早节点的后向边, 或可能到达更早节点的横叉边, 所以low(b)不能继续更新,
此时dfn(b)==low(b), 找到一个强连通分量, 将d∼b出栈.
对于节点a, 其不存在额外的边, dfn(a)==low(a), a作为一个节点数目为1
的强联通分量出栈.
算法细节
-
在low(u)更新过程中, 不考虑已经遍历且出栈节点v: 已出栈说明节点v属于其他强联通分量, 与u独立.
-
在low(u)更新过程中, 对于已遍历且仍在栈中的节点v, 用low(v)还是dfn(v)更新? 都可以, 算法在
节点回溯时才会更新对应low值, 对于节点v还没回溯, low(v)还未更新, 所以此时dfn(v)==low(v).