Tarjan 是大佬的名字, 大佬一生发明了许多算法, 都叫 Tarjan, 我会渐渐整理出来, 想知道都有啥的话, 可以直接到我文章列表搜索 Tarjan.
重温Tarjan, 网上看了许多博客感觉都讲的不清楚. 故传上来自己的笔记, 希望帮到大家.
提到的一些概念可以参考 oi wiki, 代码也是 oi wiki 的, 因为我不认为我能写出比大佬更好的代码了.
强连通分量: 有向图的最大强连通子图 ( 有向图中任意两点可达 )
-
Tarjan
-
对每个结点维护:
-
dfn[x]
: 当前节点的 dfs 序. -
low[x]
: x 向下搜索能到达的最小 dfs 序.
-
-
更新 low:
-
v 未被访问过: 初始
low[v] = dfn[v]
.v 入栈. 回溯时用 low[v] 更新它的 fa 的 low[ ]. -
v 被访问过, 且还在栈中: 用 dfs[v] 更新 fa 的 low.
-
v 被访问过, 不在栈中: 说明这是一个 fa 到 v 的单向访问, 跳过.
-
-
获取答案:
能让
dfn[x] > low[x]
, 只有当 X 的子树中某个节点 C 有 { 1. 一条横向边连接到一棵已遍历过的子树 A 2. 一条返祖边连接到 X 的祖先 x f a \begin {cases}1.一条横向边连接到一棵已遍历过的子树~A\\2.一条返祖边连接到~X~的祖先~xfa \end{cases} {1.一条横向边连接到一棵已遍历过的子树 A2.一条返祖边连接到 X 的祖先 xfa .- 横向边: 说明 A 没有连接到 C 的边, 否则在之前 C 就被遍历了, 轮不到 X 来遍历. 就用是否 C 在栈中来排除这个情况, 子树 A 中的所有强连通分量之前已经出栈过了( 看代码的实现 ).
- 返祖边: 说明 xfa -> x -> c -> xfa 形成环, 在同一个强连通子图( 我们知道, 强连通图是许多环嵌套成的 ). 而且这个子图的根是 xfa 满足
dfn[xfa] = low[xfa]
.
此时栈中进来过三类节点 :
{ 1. 在 x 的子树中 { 1. 属于上述 x f a 循环的 , 在同一个强连通子图 . 2. 不在同一个强连通子图 , 那递归的讲 , 在之前就因为属于某个 x f a ′ ( 在 X 的子树中 ) , 而被踢出栈了 . 2. 不在 x 的子树中 ( 即在已遍历过的子树中 ) , 在栈中的位置一定在 x 的下面 . \begin {cases}1.~在~x~的子树中\begin {cases}1.~属于上述~xfa~循环的,~在同一个强连通子图.\\2.~不在同一个强连通子图,~那递归的讲,~在之前就因为属于某个~xfa'~(在~X~的子树中),而被踢出栈了.\end{cases}\\2. 不在~x~的子树中(即在已遍历过的子树中),~在栈中的位置一定在~x~的下面. \end{cases} ⎩ ⎨ ⎧1. 在 x 的子树中{1. 属于上述 xfa 循环的, 在同一个强连通子图.2. 不在同一个强连通子图, 那递归的讲, 在之前就因为属于某个 xfa′ (在 X 的子树中),而被踢出栈了.2.不在 x 的子树中(即在已遍历过的子树中), 在栈中的位置一定在 x 的下面.故, 回溯时若节点符合
dfn[x] = low[x]
, 说明当前节点是它所属连通块的最小节点. 栈里它之上所有点都是一个强连通块.
-
代码:
const int Maxn = 1e5 + 10;
int dfn[Maxn], low[Maxn], dfncnt, s[Maxn], in_stack[Maxn], tp;
int scc[Maxn], sc; // 结点 i 所在 SCC 的编号
int sz[Maxn]; // 强连通 i 的大小
void tarjan(int u) {
low[u] = dfn[u] = ++dfncnt, s[++tp] = u, in_stack[u] = 1;
for (int i = head[u]; i; i = eg[i].nex) {
const int &v = eg[i].to;
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;
}
}
学习过程中还看到了另一种算法, 就简单记录一下:
- Kosaraju算法
-
对原图 G 进行深度优先遍历,记录每个节点的 dep,形成森林。
-
选择 dep 最小的顶点,对反图 GT 进行遍历,删除能够遍历到的顶点,这些顶点构成一个强连通分量
-
如果还有顶点没有删除,继续 step2,否则算法结束。
// g 是原图,g2 是反图
void dfs1(int u) {
vis[u] = true;
for (int v : g[u])
if (!vis[v]) dfs1(v);
s.push_back(u);
}
void dfs2(int u) {
color[u] = sccCnt;
for (int v : g2[u])
if (!color[v]) dfs2(v);
}
void kosaraju() {
sccCnt = 0;
for (int i = 1; i <= n; ++i)
if (!vis[i]) dfs1(i);
for (int i = n; i >= 1; --i)
if (!color[s[i]]) {
++sccCnt;
dfs2(s[i]);
}
}