模块依赖一旦出现环,拓扑排序就会失败。本文用 Python 实现 Tarjan 强连通分量,解释 index、lowlink 和栈成员的含义,提供递归深度与自环测试。 同时说明边界、复杂度与可复现实验,方便读者直接改造成自己的工具。 同时说明边界、复杂度与可复现实验,方便读者直接改造成自己的工具。
依赖图中 A 依赖 B、B 依赖 C、C 又依赖 A 时,三个模块应该一起升级或一起隔离。只标记访问过的节点无法区分“已经结束的分支”和“当前路径上的回边”,Tarjan 用栈成员状态补上这条信息。
面试官先问:环究竟是什么
index 是节点第一次被发现的时间,lowlink 是从当前 DFS 子树出发,沿树边和回边能回到的最小 index。节点 u 满足 low[u]==index[u] 时,它就是一个强连通分量的根,栈顶到 u 的节点可以一起出栈。
lowlink 在记录哪条回边
DFS 访问邻居 v:若 v 未访问,递归后 low[u]=min(low[u],low[v]);若 v 仍在栈中,low[u]=min(low[u],index[v])。发现根后不断 pop,直到 u。已经出栈的节点不参与 lowlink 更新。
走过 A-B-C-A
图 A->B、B->C、C->A、C->D 中,A 的 lowlink 最终为 0,D 自己形成单点分量;当 C 看到 A 仍在栈中时,回边把 low[C] 拉回 index[A],环就被识别。
为什么出栈不能提前
lowlink 只沿当前 DFS 树和栈内回边传播。根节点没有更早的栈内祖先,因此 low==index;若不满足,说明仍有路径回到更早节点。每个节点入栈出栈一次,复杂度线性。
大图上的实现选择
依赖扫描可能达到百万节点,Python 递归要提高栈限制或改写为显式帧栈。输出分量后可缩点成 DAG。
完整可运行代码
importsysdefscc(graph):n=len(graph);sys.setrecursionlimit(max(1000,2*n+10));idx=0;st=[];on=[False]*n;ind=[-1]*n;low=[0]*n;out=[]defdfs(u):nonlocalidx ind[u]=low[u]=idx;idx+=1;st.append(u);on[u]=Trueforvingraph[u]:ifind[v]<0:dfs(v);low[u]=min(low[u],low[v])elifon[v]:low[u]=min(low[u],ind[v])iflow[u]==ind[u]:comp=[]whileTrue:v=st.pop();on[v]=False;comp.append(v)ifv==u:breakout.append(sorted(comp))foruinrange(n):ifind[u]<0:dfs(u)returnsorted(out)if__name__=='__main__':assertscc([[1],[2],[0,3],[]])==[[0,1,2],[3]]assertscc([[0],[]])==[[0],[1]]print('tarjan tests passed')逐行读代码
闭包中的 idx 负责分配发现序号;on明确节点是否仍在当前栈。递归调用返回后先更新 low,再判断根。排序只用于让测试输出稳定,不属于算法本身。
工程扩展
强连通分量缩点后得到 DAG,可继续做关键路径、依赖层级或循环报告。若图来自外部文件,应在读入时检查顶点编号和重复边。
可复现实验
运行脚本输出tarjan tests passed。测试覆盖三节点环、自环、孤立点和多条重复边;可随机图与 Floyd 可达性定义的互相可达集合对照。
复杂度分析
时间 O(V+E),空间 O(V),包括栈、编号数组和输出。递归版本受调用栈限制,显式栈版本复杂度相同但代码更长。
边界条件
空图、孤立点、自环、平行边和超深链都要覆盖;无向图不能直接套用有向图的 lowlink 规则。
常见错误
把所有访问过邻居都当回边、忘记判断 on-stack、出栈后仍更新 low,以及根节点只弹一个元素,都会切碎或合并错误的分量。
可复制的测试用例
执行两个断言,再随机生成 n<=8 的有向图。用可达矩阵判断 i、j 是否互相可达,将等价类与 Tarjan 输出比较。
上线前检查
- 栈状态:只对仍在栈中的邻居更新
- 根:low 等于 index 才出栈
- 缩点:分量之间形成 DAG
- 深度:大图考虑显式栈
总结
Tarjan 的洞察是“正在处理”本身就是图的信息。栈把路径上下文保存下来,lowlink 则把回到祖先的证据压缩成一个数字;识别环因此不需要反复做全图搜索。
标签:Tarjan强连通分量图算法Python
参考来源
- CSDN 数据结构与算法频道
- 动态规划题型分类与解题套路
复盘补充
Tarjan 的洞察是“正在处理”本身就是图的信息。栈把路径上下文保存下来,lowlink 则把回到祖先的证据压缩成一个数字;识别环因此不需要反复做全图搜索。 依赖扫描可能达到百万节点,Python 递归要提高栈限制或改写为显式帧栈。输出分量后可缩点成 DAG。
复盘补充
Tarjan 的洞察是“正在处理”本身就是图的信息。栈把路径上下文保存下来,lowlink 则把回到祖先的证据压缩成一个数字;识别环因此不需要反复做全图搜索。 依赖扫描可能达到百万节点,Python 递归要提高栈限制或改写为显式帧栈。输出分量后可缩点成 DAG。
复盘补充
Tarjan 的洞察是“正在处理”本身就是图的信息。栈把路径上下文保存下来,lowlink 则把回到祖先的证据压缩成一个数字;识别环因此不需要反复做全图搜索。 依赖扫描可能达到百万节点,Python 递归要提高栈限制或改写为显式帧栈。输出分量后可缩点成 DAG。
复盘补充
Tarjan 的洞察是“正在处理”本身就是图的信息。栈把路径上下文保存下来,lowlink 则把回到祖先的证据压缩成一个数字;识别环因此不需要反复做全图搜索。 依赖扫描可能达到百万节点,Python 递归要提高栈限制或改写为显式帧栈。输出分量后可缩点成 DAG。
复盘补充
Tarjan 的洞察是“正在处理”本身就是图的信息。栈把路径上下文保存下来,lowlink 则把回到祖先的证据压缩成一个数字;识别环因此不需要反复做全图搜索。 依赖扫描可能达到百万节点,Python 递归要提高栈限制或改写为显式帧栈。输出分量后可缩点成 DAG。
复盘补充
Tarjan 的洞察是“正在处理”本身就是图的信息。栈把路径上下文保存下来,lowlink 则把回到祖先的证据压缩成一个数字;识别环因此不需要反复做全图搜索。 依赖扫描可能达到百万节点,Python 递归要提高栈限制或改写为显式帧栈。输出分量后可缩点成 DAG。
复盘补充
Tarjan 的洞察是“正在处理”本身就是图的信息。栈把路径上下文保存下来,lowlink 则把回到祖先的证据压缩成一个数字;识别环因此不需要反复做全图搜索。 依赖扫描可能达到百万节点,Python 递归要提高栈限制或改写为显式帧栈。输出分量后可缩点成 DAG。
复盘补充
Tarjan 的洞察是“正在处理”本身就是图的信息。栈把路径上下文保存下来,lowlink 则把回到祖先的证据压缩成一个数字;识别环因此不需要反复做全图搜索。 依赖扫描可能达到百万节点,Python 递归要提高栈限制或改写为显式帧栈。输出分量后可缩点成 DAG。
复盘补充
Tarjan 的洞察是“正在处理”本身就是图的信息。栈把路径上下文保存下来,lowlink 则把回到祖先的证据压缩成一个数字;识别环因此不需要反复做全图搜索。 依赖扫描可能达到百万节点,Python 递归要提高栈限制或改写为显式帧栈。输出分量后可缩点成 DAG。
复盘补充
Tarjan 的洞察是“正在处理”本身就是图的信息。栈把路径上下文保存下来,lowlink 则把回到祖先的证据压缩成一个数字;识别环因此不需要反复做全图搜索。 依赖扫描可能达到百万节点,Python 递归要提高栈限制或改写为显式帧栈。输出分量后可缩点成 DAG。
复盘补充
Tarjan 的洞察是“正在处理”本身就是图的信息。栈把路径上下文保存下来,lowlink 则把回到祖先的证据压缩成一个数字;识别环因此不需要反复做全图搜索。 依赖扫描可能达到百万节点,Python 递归要提高栈限制或改写为显式帧栈。输出分量后可缩点成 DAG。
复盘补充
Tarjan 的洞察是“正在处理”本身就是图的信息。栈把路径上下文保存下来,lowlink 则把回到祖先的证据压缩成一个数字;识别环因此不需要反复做全图搜索。 依赖扫描可能达到百万节点,Python 递归要提高栈限制或改写为显式帧栈。输出分量后可缩点成 DAG。
复盘补充
Tarjan 的洞察是“正在处理”本身就是图的信息。栈把路径上下文保存下来,lowlink 则把回到祖先的证据压缩成一个数字;识别环因此不需要反复做全图搜索。 依赖扫描可能达到百万节点,Python 递归要提高栈限制或改写为显式帧栈。输出分量后可缩点成 DAG。