都3202年了,你还不会强连通分量 ?!快来看看这篇文章吧
首先得先来了解一下一些概念
- 连通分量:在无向图中,即为连通子图。
- 强连通分量:有向图中,尽可能多的若干顶点组成的子图中,这些顶点都是相互可到达的,则这些顶点成为一个强连通分量。
上图中有三个强连通分量,分别是
a
,
b
,
e
a,b,e
a,b,e 以及
f
,
g
f,g
f,g 和
c
,
d
,
h
c,d,h
c,d,h。
概念了解完了,那怎么求呢?
一、 Tarjan \text{Tarjan} Tarjan 算法
Tarjan \text{Tarjan} Tarjan 算法是最常用的一种算法。
它的发明者是 Robert E. Tarjan(罗伯特·塔扬,1948~),生于美国加州波莫纳,计算机科学家。
Tarjan 发明了很多算法和数据结构。不少他发明的算法都以他的名字命名,以至于有时会让人混淆几种不同的算法。比如求各种连通分量的 Tarjan \text{Tarjan} Tarjan 算法,求 LCA(Lowest Common Ancestor,最近公共祖先) \text{LCA(Lowest Common Ancestor,最近公共祖先)} LCA(Lowest Common Ancestor,最近公共祖先) 的 Tarjan 算法。并查集、 Splay \text{Splay} Splay 、 Toptree \text{Toptree} Toptree 也是 Tarjan 发明的。
我们这里要介绍的是在有向图中求强连通分量的 Tarjan \text{Tarjan} Tarjan 算法。
但,在介绍该算法之前,先来了解 DFS \operatorname{DFS} DFS 生成树
有向图的 DFS \operatorname{DFS} DFS 生成树主要有 4 种边(不一定全部出现):
- 树边( tree edge \text{tree edge} tree edge):示意图中以黑色边表示,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。
- 反祖边( back edge \text{back edge} back edge):示意图中以红色边表示(即 7 → 1 7 \rightarrow 1 7→1),也被叫做回边,即指向祖先结点的边。
- 横叉边( cross edge \text{cross edge} cross edge):示意图中以蓝色边表示(即 9 → 7 9 \rightarrow 7 9→7),它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先。
- 前向边( forward edge \text{forward edge} forward edge):示意图中以绿色边表示(即 3 → 6 3 \rightarrow 6 3→6),它是在搜索的时候遇到子树中的结点的时候形成的。
我们考虑 DFS \operatorname{DFS} DFS 生成树与强连通分量之间的关系。
如果结点 u u u 是某个强连通分量在搜索树中遇到的第一个结点,那么这个强连通分量的其余结点肯定是在搜索树中以 u u u 为根的子树中。结点 u u u 被称为这个强连通分量的根。
Tarjan \text{Tarjan} Tarjan 算法怎么求强连通分量?
在 Tarjan \text{Tarjan} Tarjan 算法中为每个结点 u u u 维护了以下几个变量:
- dfn u \text{dfn}_u dfnu:深度优先搜索遍历时结点 u u u 被搜索的次序。
- low u \text{low}_u lowu:在 u 的子树中能够回溯到的最早的已经在栈中的结点。设以 u u u 为根的子树为 Subtree u \text{Subtree}_u Subtreeu。 low u \text{low}_u lowu 定义为以下结点的 dfn \text{dfn} dfn 的最小值: Subtree u \text{Subtree}_u Subtreeu 中的结点;从 Subtree u \text{Subtree}_u Subtreeu 通过一条不在搜索树上的边能到达的结点。
注意:
一个结点的子树内结点的
dfn
\text{dfn}
dfn 都大于该结点的
dfn
\text{dfn}
dfn。
从根开始的一条路径上的 dfn \text{dfn} dfn 严格递增, low \text{low} low 严格非降。
过程:
按照深度优先搜索算法搜索的次序对图中所有的结点进行搜索,维护每个结点的 dfn \text{dfn} dfn 与 low \text{low} low 变量,且让搜索到的结点入栈。每当找到一个强连通元素,就按照该元素包含结点数目让栈中元素出栈。在搜索过程中,对于结点 u u u 和与其相邻的结点 v v v( v v v 不是 u u u 的父节点)考虑 3 3 3 种情况:
- v v v 未被访问:继续对 v v v 进行深度搜索。在回溯过程中,用 low v \text{low}_v lowv 更新 low u \text{low}_u lowu。因为存在从 u u u 到 v v v 的直接路径,所以 v v v 能够回溯到的已经在栈中的结点, u u u 也一定能够回溯到。
- v v v 被访问过,已经在栈中:根据 t e x t l o w text{low} textlow 值的定义,用 dfn v \text{dfn}_v dfnv 更新 low u \text{low}_u lowu。
- v v v 被访问过,已不在栈中:说明 v v v 已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。
注意:下节的链接在讨论区
代码:
int dfn[N], low[N], dfncnt, s[N], in_stack[N], tp;
int scc[N], sc; // 结点 i 所在 SCC 的编号
int sz[N]; // 强连通 i 的大小
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;
}
}
那啥啥?考虑到有Py党的朋友,所以……我去学了一下Py的写法
dfn = [] * N; low = [] * N; dfncnt = 0; s = [] * N; in_stack = [] * N; tp = 0
scc = [] * N; sc = 0 # 结点 i 所在 SCC 的编号
sz = [] * N # 强连通 i 的大小
def tarjan(u):
low[u] = dfn[u] = dfncnt; s[tp] = u; in_stack[u] = 1
dfncnt = dfncnt + 1; tp = tp + 1
i = h[u]
while i:
v = e[i].t
if dfn[v] == False:
tarjan(v)
low[u] = min(low[u], low[v])
elif in_stack[v]:
low[u] = min(low[u], dfn[v])
i = e[i].nex
if dfn[u] == low[u]:
sc = sc + 1
while s[tp] != u:
scc[s[tp]] = sc
sz[sc] = sz[sc] + 1
in_stack[s[tp]] = 0
tp = tp - 1
scc[s[tp]] = sc
sz[sc] = sz[sc] + 1
in_stack[s[tp]] = 0
tp = tp - 1
下次我们继续!
参考资料: