现实世界中的很多关系天然是有方向的。一个任务的完成依赖另一个任务的前提,一门课程要求先修另一门课程,一个网页通过超链接指向另一个网页。有向图的独特之处在于,方向赋予了边以非对称语义,从而催生了无向图中不存在的结构问题。其中最重要的两个,当属拓扑排序与强连通分量。
一、有向无环图与拓扑排序
有向无环图 (DAG)是不包含任何有向环的有向图。许多实际问题天然地约束在DAG上:课程依赖关系不能出现"A依赖B、B依赖A"的死锁,项目的构建顺序不能形成循环等待。DAG的结构特性保证了一个优雅的操作------拓扑排序。
拓扑排序是指将DAG的所有顶点排列成一个线性序列,使得对于图中任意一条有向边 (u,v)(u,v),uu 在序列中都出现在 vv 之前。换言之,所有的边都沿着序列的方向"向前"指。
DAG必定存在拓扑排序,这一命题的逆也成立:一个有向图存在拓扑排序当且仅当它是DAG。证明思路简洁------若图中存在环,环上顶点互相可达,不可能排出线性顺序;若图中无环,则必然存在至少一个入度为0的顶点,将其输出并从图中删除,剩余子图仍为DAG,递归可得全序。
实际算法有两种经典实现。第一种基于入度 :计算所有顶点的入度,将入度为0的顶点加入队列,每次取出一个顶点输出,并将其所有邻接顶点的入度减1,若减至0则入队。此过程本质上就是BFS的变体,时间复杂度 Θ(n+m)Θ(n+m)。第二种基于DFS:对图进行深度优先搜索,当一个顶点的所有邻接顶点都被探索完毕(涂黑色),将该顶点加入一个栈的顶部。DFS结束后,从栈顶依次弹出顶点,得到的顺序恰好是拓扑排序的逆序------因为DFS总是先完成"下游"的顶点,后完成"上游"的顶点。
拓扑排序是DAG上一切高级算法的基础。单源最短路径在DAG上可以通过按拓扑序松弛边,以 Θ(n+m)Θ(n+m) 求出,哪怕存在负权边也不会影响正确性。最长路径问题在一般图中是NP困难的,但在DAG上同样只需一次拓扑序遍历即可解决------这正是项目关键路径分析的核心算法。
二、强连通分量与分量图
有向图中一旦出现环路,拓扑排序便不再可能。此时我们需要另一种分解工具------强连通分量 (SCC)。一个强连通分量是图的一个极大顶点子集,其中任意两个顶点互相可达。将每个强连通分量收缩为一个超顶点,原图被压缩成一张DAG,称为分量图。这种"把环捏成点"的处理,是分析含环有向图的核心技巧。
强连通分量的计算有一个经典的线性时间算法------Kosaraju算法。它仅需两遍DFS,思路极为优雅。
第一遍DFS:在原图上运行完整的DFS,记录每个顶点完成探索的时刻(即涂黑色的时间戳 f[v]f[v])。顶点完成的顺序形成了一个特定的时间序列。
第二遍DFS :将原图所有边的方向反转,得到转置图 GTGT。按照第一遍DFS的完成时间递减顺序,在转置图上依次对尚未访问的顶点启动DFS。每一棵新生成的深度优先树(即每次调用DFS所能到达的所有顶点),恰好构成一个强连通分量。
为什么这样可以得到SCC?直观解释是:第一遍DFS的完成时间将分量间的依赖关系"拓扑化"了------汇分量(没有外向边指向其他SCC的分量)先完成,源分量(没有其他SCC指向它的分量)后完成。转置图颠倒了边的方向,使得源分量变成"无法逃离"的闭包。按完成时间递减序在转置图上启动DFS,第一次调用便恰好卡在一个源分量(在转置图中变为汇分量)内部,无法到达其他分量,从而精确地将该分量从图中剥离。这一推理严格证明了Kosaraju算法的正确性。
Kosaraju算法的总时间复杂度为 Θ(n+m)Θ(n+m)------两遍DFS加一次图转置,均为线性。
三、应用场景:从编译到推荐系统
拓扑排序和强连通分量分解在实际系统中有广泛应用。
在编译器中,链接器需要确定目标文件的链接顺序,头文件之间的包含关系构成DAG,拓扑排序给出合法的编译顺序。Make和CMake等构建系统的依赖解析核心正是这一算法。
在任务调度中,项目评估与审查技术(PERT)用DAG建模任务间的先后依赖,DAG上的最长路径对应项目的关键路径,任何关键路径上任务的延迟都将推迟整个项目的交付。
在复杂网络分析中,Web图的强连通分量分解揭示了互联网的"蝴蝶结"结构------一个巨大的核心SCC,加上进入核心的"IN"区域和从核心指出的"OUT"区域,以及边缘的须状结构。类似的分析方法也被用于社交网络的社区发现和推荐系统的信任传播建模。
在2-SAT问题的求解中,将每个布尔变量的两种取值映射为有向图中的顶点,蕴含关系映射为边,图的强连通分量直接给出了公式的可满足性判定------变量与其否定出现在同一个SCC中当且仅当公式不可满足。这个化归使得2-SAT可以在线性时间内解决。
四、后续议题的衔接
拓扑排序为我们提供了无环有向图的线性化视角,强连通分量将有环图规约为DAG。下一篇,我们将延续这种"从复杂中提取简单结构"的思路,进入带权图的最小生成树问题------在无向连通图中寻找连接所有顶点的最小代价边集,并以Prim算法和Kruskal算法为核心展开比较分析。