某大学计算机专业有一份培养方案,课程之间有先修关系:比如"学数据结构之前必须先学程序设计"、"学算法分析之前必须先学数据结构和离散数学"、"毕业设计必须在所有专业课之后"。教务处需要回答一个简单的问题:能不能把这些课排成一条线性顺序,使得每一门课都排在它所有先修课之后?
教务处的小陈把这个问题画成了一张有向图:每门课是一个点,如果"课程 A 是课程 B 的先修课",就画一条有向边 A → B。于是问题变成了"给这张图的所有点排个序,使得每条边都是从前往后指的" —— 这就是拓扑排序(Topological Sort)。
小陈很快发现了一个前提:如果课程之间出现了循环依赖(A 要求先学 B、B 要求先学 C、C 又要求先学 A),那就永远排不出来。所以拓扑排序只对有向无环图(DAG)有意义 —— 而这个"排不出来"本身,恰好就是判断图中有没有环的最简单方法。
同一天,工地的项目经理想问一个"升级版"问题:把每道工序看作一个点,工序之间的先后关系看作有向边,每道工序有耗时。他想知道整个工程最短需要多少天。小陈笑了:这不就是"在 DAG 上求最长路"吗?先做拓扑排序,再顺着拓扑序做一遍 DP 就行了 —— 拓扑序让 DP 有了"无后效性"。
拓扑排序是把"偏序"铺平成一"条线",铺平之后,DP 就能顺着走下去。
💡 故事背后的算法思想
Kahn 算法的直觉:"没有先修要求的课"(入度为 0 的点)可以马上去上。上完之后,它作为先修课的那些课,先修要求就少了一个;当某门课的所有先修要求都被满足(入度归零)时,它也可以上了。用队列维护"当前可上的课",不断取出、删除出边、检查新的入度归零点。
DFS 逆后序的直觉:深度优先遍历时,一个点在"它的所有后继都访问完毕"之后才被"完成"。把完成顺序倒过来,就得到了拓扑序 —— 因为"后继先完成",倒过来就是"后继排在前面"。
为什么拓扑序这么重要:在有环的图上,"从 A 走到 B"可能无限绕圈,DP 的状态会互相依赖、无法确定递推顺序。而 DAG 上的拓扑序保证了"所有能到达 v 的点都排在 v 前面",于是只要按拓扑序推进,每个状态在被用到时都已经算好了 —— 这就是无后效性。