在图算法的领域中,拓扑排序为有向无环图(DAG)的顶点提供了一种线性排序方式。今天,唐叔将带领你探索拓扑排序的神秘世界,了解其在解决实际问题中的强大应用。通过本文,你将学会如何运用拓扑排序来处理图相关问题,并在LeetCode上的应用实例中,掌握其解题技巧。
一、什么是拓扑排序?
定义
拓扑排序是对有向无环图(DAG, Directed Acyclic Graph)中的顶点进行线性排序,使得对于每一个有向边 u -> v,顶点 u 在排序结果中都出现在顶点 v 之前。换句话说,它是一种能够反映节点之间先后顺序关系的排序方法。不过,需要注意:这种排序不是唯一的,因为图中可能存在多个有效的拓扑排序。
应用场景
拓扑排序常用于解决以下问题:
- 任务调度:在有依赖关系的任务中,确定执行顺序。
- 课程安排:根据先修课要求确定学生选课的顺序。
- 编译器优化:决定函数调用或指令执行的次序。
- 项目规划:安排任务的时间表,确保所有前置任务完成后再开始后续工作。
- 数据流分析:分析数据流之间的依赖关系,并确定数据的处理顺序。
算法实现
最常用的两种算法是Kahn算法和深度优先搜索(DFS)。前者基于入度计算,后者则利用了递归回溯的思想。PS:有关DFS和回溯算法相关内容,详情见往期文章。【唐叔学算法】第11天:深度优先遍历-探索图与树的神秘深处。
Kahn算法步骤
- 计算每个顶点的入度。
- 将所有入度为0的顶点加入队列。
- 取出队首元素,并将其指向的所有顶点的入度减1;若某顶点入度变为0,则将其加入队列。
- 重复第3步直到队列为空。
- 如果输出的顶点数等于原图顶点数,则存在合法的拓扑序列;否则,图中存在环。
DFS算法步骤
- 对于每个未访问过的顶点,启动一次深度优先遍历。
- 在回溯时将当前顶点添加到结果列表中。
- 最终的结果列表逆序即为拓扑排序。
注意事项
- 检测环路:如果在执行过程中发现无法继续或者处理完毕后仍有剩余顶点未被包含进来,说明原图含有环。
- 唯一性问题:当图中某些顶点间不存在直接或间接的依赖关系时,可能得到多个不同的有效拓扑序列。
- 注意处理图中的入度为0的节点,这些节点可以作为拓扑排序的起点。
二、实战解析
入门题:207. 课程表
题目链接:207. 课程表
题目描述:现在你总共有 n 门课需要选,记为 0
到 n-1
。给定一个数组 prerequisites
,其中每个元素是一个长度为 2 的数组 [a, b]
表示想要学习课程 a 必须先完成课程 b。请判断是否可以完成所有课程的学习。
解题思路
这个问题可以通过拓扑排序来解决。我们可以使用Kahn算法或DFS来尝试对课程进行排序。如果最终能成功排出所有课程,则说明不存在循环依赖,可以完成所有课程的学习;反之则不能。
Java代码实现(采用Kahn算法)
import java.util.*;
public class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < numCourses; ++i)
adj.add(new ArrayList<>());
int[] indegree = new int[numCourses];
// 构建邻接表并计算入度
for (int[] edge : prerequisites) {
adj.get(edge[1]).add(edge[0]);
indegree[edge