交互式图论学习
交互式图论学习
Guest User
Using app without sign in
拓扑顺序生成器
有向无环图中顶点的线性排序
选择算法并生成步骤以开始可视化
拓扑排序为有向无环图(DAG)的顶点生成一个线性顺序,使每条边都从较前的顶点指向较后的顶点。它回答的问题是:当某些任务依赖于其他任务时,任务可以按什么顺序执行?
有两种标准方法。Kahn 算法反复移除一个没有入边的顶点,把它追加到顺序中,并将其邻居的入度减一;队列保存当前入度为零的顶点。DFS 方法执行深度优先搜索,按完成时间的逆序输出顶点。两者都运行在 O(V + E)。若有顶点未被处理(Kahn)或出现回边(DFS),则图中有环,不存在有效顺序。
拓扑顺序为 Make 和 Gradle 等构建系统排程、解析包安装顺序、为有先修要求的大学课程排序、安排电子表格单元格的求值顺序,并在编译器中调度指令执行。它是有向图中最常见的中等难度面试题之一。
两种标准写法,都是线性的。Kahn 从没有前置依赖的结点开始正向推进;DFS 版本则借助完成时间反向推导。
// Kahn:反复移除一个没有入边的结点
计算每个结点的 入度[v]
队列 = 所有入度为 0 的结点
顺序 = []
当队列非空时:
u = 队列.取出()
顺序.追加(u)
对每条边 (u, v):
入度[v] -= 1
若 入度[v] == 0: 队列.放入(v)
若 顺序.长度 < V: 图中存在环
// DFS 变体:完成顺序的逆序
执行 DFS;每个结点完成时压入栈中
把栈弹空所得到的序列就是一个合法的拓扑序Kahn 有一个值得了解的实用优势:由于它是通过统计成功输出了多少结点来判断有没有环的,剩下未输出的那些结点恰好就是位于环中或位于环下游的结点。当你需要报告究竟是哪些依赖构成了循环时,这比一个布尔值有用得多。
在一个小型构建依赖图上运行 Kahn,其中边 X 到 Y 表示 X 必须先于 Y 构建。
示例图: 有向边 A 到 C、B 到 C、C 到 D 和 B 到 D。
一个合法的顺序是 A、B、C、D。请注意 B、A、C、D 同样合法:A 与 B 都没有前置依赖,它们之间的相对顺序不受约束。除非整个图是一条链,否则拓扑序并不唯一,因此测试应当验证输出中每条边都指向前方,而不是与某个预期序列逐一比对。
时间: O(V + E) · 空间: O(V)
计算全部入度需要扫一遍所有边,为 O(E)。每个结点恰好入队一次、出队一次,为 O(V)。每条边也恰好被检查一次,即在其起点被输出、终点入度递减的时候,又是 O(E)。空间用于保存入度数组、队列和输出列表,都是 O(V)。DFS 变体的界限相同,只是用递归栈代替了队列。两者都无法再改进,因为任何正确的算法都必须读完所有边才能知道全部约束。
Kahn 与 DFS 给出的顺序同样合法。选哪个取决于你在排序之外还需要什么。
| 替代算法 | 以下情况更合适 | 代价 |
|---|---|---|
| Kahn(BFS 风格) | 你需要环的诊断信息,或者需要借助优先队列得到字典序最小的顺序,又或者必须避免深递归。 | O(V + E) |
| DFS 完成顺序 | 你本来就要跑一次 DFS,或者你想要最简短的实现。 | O(V + E) |
| Tarjan 强连通分量 | 图中有环,而你希望把它们缩成一个 DAG 而不是直接拒绝输入。 | O(V + E) |
| 最长路径 / CPM | 结点带有工期而你要的是关键路径。那就是拓扑排序之后再加一趟动态规划。 | O(V + E) |
阅读完整文章: Graph Algorithms in Coding Interviews
相关算法: 深度优先搜索, 环检测, 关键路径法 (CPM)