浅谈 Kosaraju

· · 算法·理论

https://www.cnblogs.com/linhanmo/articles/19974910/kosaraju

前言

我们求强连通分量时会发现 Tarjan 太长了,其实有一个好写的算法 Kosaraju。

强连通分量

一个图是强连通的当且仅当一个图的任意两个节点都能互相到达。

强连通分量指一个图里极大的(不能在扩张的)强连通子图。

Kosaraju 算法

算法流程

两次 dfs:

  1. 选一个未访问过的(不保证连通)的点作为起点,求出后序遍历。直到都被访问过

  2. 对于反图,从后序遍历标号最大且未访问过的的顶点作为起点开始 dfs,遍历到的就是强连通分量。直到都被访问过

代码见例题 P2863。

正确性证明

必要性

对于第二次 DFS 的起点记为 T,访问到的记为 S

原图 S 可达 T
$\therefore$ 原图 $S$ 可达 $T$。 ##### 原图 $T$ 可达 $S$: $\because T$ 后序遍历标号比 $S$ 大: ::::success[$T$ 在 DFS 树上是 $S$ 的祖先] 显然 $T$ 可达 $S$。 :::: ::::error[$T$ 和 $S$ 在不同的子树且 $S$ 的子树先访问到] $\because T$ 反图能到达 $S$: $\therefore$ 原图 $S$ 可达 $T$; 又 $\because S$ 的子树先遍历到: $\therefore S$ 比 $T$ 不超过 $S,T$ 的 LCA 的最靠近根节点的祖先先遍历到; $\therefore$ DFS 树上是 $S$ 是 $T$ 的祖先,矛盾! 故不可能存在这种情况。 :::: #### 充分性 反证法。 假设在反图上存在一点 $S$ 和 $T$ 属于同一个强连通分量但 $S$ 并没有被 $T$ 遍历到。 ::::error[$T$ 尚未被标记强连通分量且 $S$ 错过了 $T$] $\because$ 原图 $S$ 可达 $T$: $\therefore$ 反图 $T$ 可达 $S$; 又 $\because$ $T$ 尚未被标记强连通分量; $\therefore S$ 必然能标记 $T$。 故不存在这种情况。 :::: ::::error[在 $S$ 遍历到 $T$ 之前已经被 $P$ 遍历到了] $\because T$ 被 $P$ 遍历到了: $\therefore P,T$ 属于同一个强连通分量; 又 $\because S,T$ 属于同一个强连通分量; $\therefore P,S$ 属于同意个强连通分量; 又 $\because P$ 的后序遍历标号比 $S$ 大; $\therefore S$ 被 $P$ 标记,与 $S$ 仍然可以遍历矛盾。 故不存在这种情况。 :::: ### 应用 #### 缩点 将整个强连通分量里的点压缩成一个点。 #### [2-SAT](https://www.luogu.com.cn/problem/solution/P4782) Tarjan 找环改为 Kosaraju 找环即可。 #### bitset 优化 发现最耗时的于当前节点 $u$,找出所有‌尚未访问‌且‌与 $u$ 有边相连‌的节点。 假如把邻接矩阵和访问情况都压成 bitset,这一步可以用与运算和 ```_Find_first``` 优化到 $O(\frac{n}{w})$。 总时间复杂度为 $O(\frac{n^2}{w})$,适用于可以用特殊方式生成边且 $m$ 接近 $n^2$ 的场景。 :::success[核心代码] ```cpp void dfsG(int u) { nvs[u] = false; while ((tmp = nvs & G[u]).any()) dfsG(tmp._Find_first()); vec[++sz] = u; } void dfsH(int u) { nvs[u] = false; while ((tmp = nvs & H[u]).any()) dfsH(tmp._Find_first()); ++sz; } ``` ::: ### 例题 #### [P2863](https://www.luogu.com.cn/problem/P2863) :::success[板子套用即可] ```cpp #include <stack> #include <vector> #include <bitset> #include <stdio.h> constexpr int N = 10010; int n, m, cnt, scc[N], sz[N], t; std::bitset<N> v; std::vector<int> e[N], re[N]; std::stack<int> s; void dfs(int a) { v.set(a); for (int b : e[a]) if (!v[b]) dfs(b); return s.push(a); } void rdfs(int a) { scc[a] = cnt, ++sz[cnt]; for (int b : re[a]) if (scc[b] == 0) rdfs(b); return; } inline void kosaraju(void) { for (int i = 1; i <= n; ++i) if (!v[i]) dfs(i); for (; !s.empty(); s.pop()) if (scc[s.top()] == 0) ++cnt, rdfs(s.top()); return; } int main(void) { scanf("%d %d", &n, &m); for (int a, b, i = m; i--;) scanf("%d %d", &a, &b), e[a].emplace_back(b), re[b].emplace_back(a); kosaraju(); int ans = 0; for (int i = 1; i <= cnt; ++i) if (sz[i] > 1) ++ans; printf("%d", ans); return 0; } ``` ::: #### [P2341](https://www.luogu.com.cn/problem/P2341) 显然同一个强连通分量里的奶牛互相受欢迎。 那么假如一个强连通分量的出度 $>0$ 那么它必然没有入度(否则就不极大了)。 所以,只有**唯一的**一个强连通分量里的奶牛是明星。 假如有多个强连通分量出度 $=0$,那么就没有明星。 :::success[代码] ```cpp #include <stack> #include <vector> #include <bitset> #include <stdio.h> constexpr int N = 10001; int n, m, cnt, scc[N], sz[N], t; std::vector<int> e[N], re[N]; std::bitset<N> v, d; std::stack<int> s; void dfs(int a) { v.set(a); for (int b : e[a]) if (!v[b]) dfs(b); return s.push(a); } void rdfs(int a) { scc[a] = cnt, ++sz[cnt]; for (int b : re[a]) if (scc[b] == 0) rdfs(b); return; } inline void kosaraju(void) { for (int i = 1; i <= n; ++i) if (!v[i]) dfs(i); for (; !s.empty(); s.pop()) if (scc[s.top()] == 0) ++cnt, rdfs(s.top()); return; } int main(void) { scanf("%d %d", &n, &m); for (int a, b, i = m; i--;) scanf("%d %d", &a, &b), e[a].emplace_back(b), re[b].emplace_back(a); kosaraju(); for (int a = 1; a <= n; ++a) for (int b : e[a]) { if (d[scc[a]]) break; if (scc[a] != scc[b]) d.set(scc[a]); } for (int i = 1; i <= cnt; ++i) if (!d[i]) { if (t) return putchar(48), 0; t = sz[i]; } printf("%d", t); return 0; } ``` ::: #### [QOJ 12805](https://qoj.ac/problem/12805/) 求互相可达的点的对数。 显然就是求强连通分量,每个强连通分量对答案的贡献为 $\frac{S(S-1)}{2}$。 bitset 优化即可。 :::success[板子套用] ```cpp #include <bits/stdc++.h> using namespace std; using ll = long long; #define For(i, st, ed) for (register int i = (st); i <= (ed); ++i) template<typename type> inline void read(type &x) { int f = 0, ch; while (ch = getchar(), ch < 48 || ch > 57) f ^= ch == 45; x = 0; do x = x * 10 + (ch ^ 48); while (ch = getchar(), ch > 47 && ch < 58); f ? x = -x : 0; } constexpr int N = 260; int n, m, q, ans, sz, vec[N]; char e[N]; bitset<N> G[N], H[N], nvs, tmp; void dfsG(int u) { nvs[u] = false; while ((tmp = nvs & G[u]).any()) dfsG(tmp._Find_first()); vec[++sz] = u; } void dfsH(int u) { nvs[u] = false; while ((tmp = nvs & H[u]).any()) dfsH(tmp._Find_first()); ++sz; } int kosaraju() { sz = ans = 0, nvs.set(); For(i, 1, n) nvs[i] ? dfsG(i) : void(); nvs.set(); for (register int i = n; i >= 1; --i) if (nvs[vec[i]]) sz = 0, dfsH(vec[i]), ans += sz * (sz - 1) / 2; return ans; } signed main() { read(n), read(q); For(i, 1, n) { scanf("%s", e + 1); For(j, 1, n) G[i][j] = H[j][i] = e[j] ^ '0'; } For(Q, 1, q) { read(m); For(M, 1, m) { int u, v; read(u), read(v), G[u].flip(v), H[v].flip(u); } printf("%d\n", kosaraju()); } return 0; } ``` ::: ### [P1653](https://www.luogu.com.cn/problem/P1653) 连高度从大到小的单向边,kosaraju 缩点。 统计入度出度,求出无入边的块数和无出边的块数即为答案。 ### [P1407](https://www.luogu.com.cn/problem/P1407) 将初始的情侣看成一个点,kosaraju 找强连通分量。 若大小等于一,安全,否则不安全。 ## 优缺点 优点: - 代码短,好记好写。 - bitset 优化。 缺点: - 除 bitset 外功能被 tarjan 偏序。 - 不考。 ## 后记 字数统计:5587 字符。 - update on 2026.5.25:添加 2-SAT 和 bitset 优化。 - update on 2026.8.10:添加 bitset 优化的代码与例题、更多例题、优缺点。