在做 ARC069F Flags 时看到有一个用 kosaraju 的 nb 做法,于是研究了一下 kosaraju。
Kosaraju 算法
kosaraju 算法是一种找出强连通分量的算法,用途和 tarjan 类似,但是代码更好写,并且在某些题上比 tarjan 算法有更多性质。
算法流程
- 对原图做一次 DFS:
- 初始化一个栈 \(S\),
- 对图中每个点未访问的点都 DFS 一遍,DFS 的时候像求 DFS 树一样,依次遍历 \(u\) 连接的没访问过的点 \(v\),并递归对 \(v\) 进行 DFS \(v\)。当 \(u\) 所有后继点都遍历完后,往栈 \(S\) 的栈顶加入 \(u\)。
 
- 按照栈中的顺序,在反图上做 DFS:
- 维护一个标记数组以记录每个点所属的 SCC 编号。
- 按照栈 \(S\) 中从栈顶到栈底的顺序依次取出结点 \(u\),若 \(u\) 未被标记则在反图上 DFS,并把每个 \(u\) 能到达的后继点的 SCC 编号标记为相同的编号。
- 按照这个逻辑,则每次在反图上的 DFS 都能找出一个完整的 SCC。
 
代码:
//adj 是原图,nadj 是反图。void dfs1(int x) {vis[x] = 1;for (auto y : adj[x]) {if (!vis[y]) {dfs1(y);}}stk.push(x);
}
void dfs2(int x) {scc[x] = cid;for (auto y : nadj[x]) {if (!scc[y]) {dfs2(y);}}
}
void kosaraju() {for (int i = 1; i <= n; ++i) {if (!vis[i]) {dfs1(i);}}while (!stk.empty()) {int u = stk.top();stk.pop();if (!scc[u]) {++cid;dfs2(u);}}
}
算法正确性证明
引理:记结点 \(u\) 在栈中的位置为 \(f_u\)(栈顶的 \(f\) 值为 \(n\),栈底的 \(f\) 值为 \(1\)),所属的强连通分量编号为 \(C_u\),那么对于原图的一条边 \(u \to v\),若满足 \(C_u \ne C_v\),则有 \(f_u > f_v\) 成立。
证明:可以把每条跨 SCC 的边 \(u \to v\) 分为:\(u, v\) 在同一个 DFS 中遍历到的,和在不同 DFS 中遍历到的。(DFS 指代码中的
dfs1)
对于被同一个 DFS 遍历到的,一定是先经过 \(u\) 再经过 \(v\),由于是后缀顺序遍历,所以越晚遍历到的点 \(f\) 值越小,故 \(f_u > f_v\) 成立。
对于被不同 DFS 遍历到的,一定是先经过 \(v\) 再经过 \(u\),由于它们属于不同的 DFS 过程,先被 DFS 的 \(f\) 一定小,故 \(f_u > f_v\) 成立。
综上,原命题成立。
我们按照 \(f\) 从大到小在反图上 DFS,每次都只会遍历栈顶 \(u\) 所属强连通分量 \(C_u\) 中的点,因为 \(C_u\) 在 DAG 上的前驱点的 \(f\) 值都比 \(f_u\) 大,所以一定都在 \(u\) 之前被遍历过了。
可以注意到我们在根据拓扑序访问 DAG 上的强连通分量,所以求出的 SCC 编号从小到大排列就是 DAG 的拓扑序。(注意如果该算法用来实现 2-SAT,要与 Tarjan 算法的写法区分)
Flags
题意
给定 \(n\) 个二元组 \((x_i, y_i)\) 和 \(n\) 个数轴上的点,第 \(i\) 个点可以取在 \(x_i\) 或 \(y_i\),你需要求出一个方案使得 \(n\) 个点中相邻两个点的最小距离最大。
很明显可以先二分答案,然后转化成一个 2-SAT 问题,朴素的做法就是线段树优化建图 + Tarjan。
但是我们可以使用 Kosaraju,Kosaraju 在优化建图的问题上与 Tarjan 的区别就在于,Kosaraju 的形式很简洁,使得我们能够直接用数据结构优化 DFS 找点的过程,而不用像 Tarjan 一样建立虚点。通常情况下 Kosaraju 的时空常数都更好,部分情况下 Kosaraju 有复杂度更好的做法(有个点分树优化建图的题 Kosaraju 的复杂度更好,懒得放了)。