lowu:在 u 的子树中能够回溯到的最早的已经在栈中的结点。设以 u 为根的子树为 Subtreeu。lowu 定义为以下结点的 dfn 的最小值:Subtreeu 中的结点;从 Subtreeu 通过一条不在搜索树上的边能到达的结点。
一个结点的子树内结点的 dfn 都大于该结点的 dfn。
从根开始的一条路径上的 dfn 严格递增,low 严格非降。
按照深度优先搜索算法搜索的次序对图中所有的结点进行搜索,维护每个结点的 dfn 与 low 变量,且让搜索到的结点入栈。每当找到一个强连通元素,就按照该元素包含结点数目让栈中元素出栈。在搜索过程中,对于结点 u 和与其相邻的结点 v(v 不是 u 的父节点)考虑 3 种情况:
v 未被访问:继续对 v 进行深度搜索。在回溯过程中,用 lowv 更新 lowu。因为存在从 u 到 v 的直接路径,所以 v 能够回溯到的已经在栈中的结点,u 也一定能够回溯到。
v 被访问过,已经在栈中:根据 low 值的定义,用 dfnv 更新 lowu。
v 被访问过,已不在栈中:说明 v 已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。
将上述算法写成伪代码:
实现
TARJAN_SEARCH(int u) vis[u]=true low[u]=dfn[u]=++dfncnt push u to the stack for each (u,v) then do if v hasn't been searched then TARJAN_SEARCH(v) // 搜索 low[u]=min(low[u],low[v]) // 回溯 else if v has been in the stack then low[u]=min(low[u],dfn[v])
对于一个连通分量图,我们很容易想到,在该连通图中有且仅有一个 u 使得 dfnu=lowu。该结点一定是在深度遍历的过程中,该连通分量中第一个被访问过的结点,因为它的 dfn 和 low 值最小,不会被该连通分量中的其他结点所影响。
因此,在回溯的过程中,判定 dfnu=lowu 是否成立,如果成立,则栈中 u 及其上方的结点构成一个 SCC。
实现
[list2tab]
C++
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; do { scc[s[tp]] = sc; sz[sc]++; in_stack[s[tp]] = 0; } while (s[tp--] != u); }}
// 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]); }}
Python
def dfs1(u): vis[u] = True for v in g[u]: if vis[v] == False: dfs1(v) s.append(u)def dfs2(u): color[u] = sccCnt for v in g2[u]: if color[v] == False: dfs2(v)def kosaraju(u): sccCnt = 0 for i in range(1, n + 1): if vis[i] == False: dfs1(i) for i in range(n, 0, -1): if color[s[i]] == False: sccCnt = sccCnt + 1 dfs2(s[i])
Garbow 算法
过程
Garbow 算法是 Tarjan 算法的另一种实现,Tarjan 算法是用 dfn 和 low 来计算强连通分量的根,Garbow 维护一个节点栈,并用第二个栈来确定何时从第一个栈中弹出属于同一个强连通分量的节点。从节点 w 开始的 DFS 过程中,当一条路径显示这组节点都属于同一个强连通分量时,只要栈顶节点的访问时间大于根节点 w 的访问时间,就从第二个栈中弹出这个节点,那么最后只留下根节点 w。在这个过程中每一个被弹出的节点都属于同一个强连通分量。
当回溯到某一个节点 w 时,如果这个节点在第二个栈的顶部,就说明这个节点是强连通分量的起始节点,在这个节点之后搜索到的那些节点都属于同一个强连通分量,于是从第一个栈中弹出那些节点,构成强连通分量。
实现
[list2tab]
C++
int garbow(int u) { stack1[++p1] = u; stack2[++p2] = u; low[u] = ++dfs_clock; for (int i = head[u]; i; i = e[i].next) { int v = e[i].to; if (!low[v]) garbow(v); else if (!sccno[v]) while (low[stack2[p2]] > low[v]) p2--; } if (stack2[p2] == u) { p2--; scc_cnt++; do { sccno[stack1[p1]] = scc_cnt; // all_scc[scc_cnt] ++; } while (stack1[p1--] != u); } return 0;}void find_scc(int n) { dfs_clock = scc_cnt = 0; p1 = p2 = 0; memset(sccno, 0, sizeof(sccno)); memset(low, 0, sizeof(low)); for (int i = 1; i <= n; i++) if (!low[i]) garbow(i);}
Python
def garbow(u): stack1[p1] = u stack2[p2] = u p1 = p1 + 1 p2 = p2 + 1 low[u] = dfs_clock dfs_clock = dfs_clock + 1 i = head[u] while i: v = e[i].to if low[v] == False: garbow(v) elif sccno[v] == False: while low[stack2[p2]] > low[v]: p2 = p2 - 1 if stack2[p2] == u: p2 = p2 - 1 scc_cnt = scc_cnt + 1 while stack1[p1] != u: p1 = p1 - 1 sccno[stack1[p1]] = scc_cntdef find_scc(n): dfs_clock = scc_cnt = 0 p1 = p2 = 0 sccno = [] low = [] for i in range(1, n + 1): if low[i] == False: garbow(i)