Kosaraju算法通过两次DFS在O(V+E)内准确求出所有SCC:第一次在原图DFS记录完成时间,第二次在转置图中按完成时间逆序DFS,每次新树即一个SCC。

用Kosaraju算法一次性找出所有SCC
最直接可靠的做法是用两次DFS的Kosaraju算法,它逻辑清晰、容易调试,且能保证线性时间复杂度 O(V + E)。关键在于第二次DFS必须按第一次DFS完成时间的逆序遍历节点。
常见错误是把第二次DFS的遍历顺序搞反——不是按原图的邻接表顺序,而是按第一次DFS中每个节点的finish_time降序排列后的顺序。如果用栈保存退出顺序,弹出顺序就是逆序;如果用vector记录,要反向遍历。
- 第一次DFS跑原图
G,记录每个节点的完成时间(或直接压栈) - 构建转置图
G_T(所有边u → v变成v → u) - 第二次DFS在
G_T上,按第一次的完成时间逆序访问节点,每次新DFS树就是一个SCC
Tarjan算法更省内存但递归细节多
Tarjan用一次DFS+栈实现,空间上省掉转置图存储,但需要维护 disc(发现时间)、low(能回溯到的最早节点时间)、以及一个栈记录当前路径上的节点。容易出错的地方集中在low更新逻辑和栈弹出时机。
典型错误:在遇到已访问但未出栈的节点时,只更新 low[u] = min(low[u], disc[v]),却漏掉判断 v 是否在栈中(要用额外布尔数组 onStack[] 标记);或者弹出栈时没把当前节点也包含进去。
立即学习“C++免费学习笔记(深入)”;
-
low[u]更新必须区分两种情况:v未访问 → 递归后更新;v已访问且在栈中 → 直接用disc[v] - 当
disc[u] == low[u]时,从栈中持续弹出直到弹出u,这些节点构成一个SCC - 别忘了每次进入DFS前给
disc[u]和low[u]赋初值,并标记onStack[u] = true
使用Boost Graph Library时注意边方向和组件索引
如果项目允许依赖第三方库,boost::strong_components() 是最省事的选择,但它返回的是每个节点所属SCC的编号(从0开始),不是SCC列表本身。你需要自己按编号分组。
容易被忽略的是:Boost默认把图当作无向图处理,必须显式传入有向图类型 boost::directedS,否则结果完全错误。另外,它的内部实现基于Kosaraju,对稀疏图友好,但若节点数超百万,手动实现Tarjan可能更可控。
- 构造图时确保用
boost::adjacency_list<:vecs boost::vecs boost::directeds></:vecs> - 调用
boost::strong_components(g, &component_id[0])后,遍历component_id数组做分组 - 返回的SCC数量等于
*max_element(component_id.begin(), component_id.end()) + 1
测试时别只用连通小图验证
很多实现能在环状、链状小图上跑通,但在含多个嵌套环+桥边的图上失败。推荐三类必测case:
- 单个环:
0→1→2→0→ 应得1个SCC - 两个不相交环:
0→1→0和2→3→2→ 应得2个SCC - 带桥的复合结构:
0→1→2→3→1,再加3→4→5→4→ 应得2个SCC({0} 和 {1,2,3}?不对,{1,2,3} 是一个,{4,5} 是另一个,0单独?等等——这里0只能到达自己,所以是3个SCC)
真正容易翻车的是节点入度为0但属于某个SCC内部的情况(比如环里某点被外部单向指向),这种结构会让朴素DFS误判。验证时最好打印每个SCC的节点集合,再人工检查是否满足“任意两点双向可达”这一定义。


















