LeetCode 207. 课程表
题目描述
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1。
在选修某些课程之前需要一些先修课程。先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai ,则 必须 先学习课程 bi。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。这是可能的。
示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这形成了一个死循环,是不可能的。
解题过程
这个问题可以被建模为一个有向图。在这个图中:
- 每个节点代表一门课程。
- 一条有向边从课程
B指向课程A,表示B是A的先修课程(即prerequisites[i] = [A, B]对应边B -> A)。
那么,问题“是否可能完成所有课程的学习”就等价于另一个图论问题:这个有向图中是否存在环? 如果存在环(如示例2中的 0 -> 1 -> 0),则说明课程之间存在循环依赖,无法完成所有课程。如果不存在环(即这是一个有向无环图,DAG),则可以完成。
我们可以使用拓扑排序 来判断一个有向图是否有环。拓扑排序是针对有向无环图(DAG)的一种线性排序,使得对于任何有向边 u -> v,在排序中 u 都出现在 v 的前面。这个顺序正好对应了我们的选课顺序。
这里我们采用 Kahn算法(基于BFS的拓扑排序),其核心思想是不断选择入度为0的节点(即没有先修课程的课程)。
步骤详解:
-
构建图并计算入度:
-
我们需要一个数据结构(如邻接表)来表示图,记录每个节点的所有后继节点(即学完这门课后,可以解锁哪些新课)。
-
同时,我们需要一个数组
inDegree来记录每个节点的入度(即这门课程有多少门先修课)。 -
具体操作:
- 初始化一个邻接表
graph,大小为课程数量numCourses,每个元素是一个空列表。 - 初始化一个入度数组
inDegree,大小为numCourses,所有元素初始化为0。 - 遍历
prerequisites数组中的每一个先修条件[ai, bi]:- 在邻接表中,将
ai加入到graph[bi]的末尾。这表示修完bi后,可以解锁ai。 - 将课程
ai的入度inDegree[ai]加1。
- 在邻接表中,将
- 初始化一个邻接表
-
-
初始化队列:
- 初始化一个队列(可以是普通的FIFO队列),用于存放当前所有入度为0的节点。
- 遍历所有课程
i(从0到numCourses-1),如果inDegree[i] == 0,说明课程i没有先修要求,可以直接学习,将其加入队列。
-
进行BFS(广度优先搜索):
- 初始化一个计数器
count为0,用于记录我们已经成功“学完”(从图中移除)的课程数量。 - 当队列不为空时:
- 从队列中取出一个节点(课程)
course。这个节点是当前可以学习的课程。 - 将计数器
count加1,表示我们学完了一门课。 - 遍历这门课程
course的所有后继课程nextCourse(即在邻接表graph[course]中):- 将每个后继课程
nextCourse的入度减1。因为这门前置课程course已经被“学完”了,相当于移除了它对后继课程的依赖。 - 检查减1之后,
nextCourse的入度是否变为0。如果变为0,说明nextCourse所有的先修课程都已完成,现在它可以被学习了,将其加入队列。
- 将每个后继课程
- 从队列中取出一个节点(课程)
- 初始化一个计数器
-
判断结果:
- 当队列为空后,BFS过程结束。
- 此时,检查计数器
count是否等于总课程数numCourses。- 如果
count == numCourses:说明所有课程都成功被“学完”了,即图中不存在环,返回true。 - 如果
count < numCourses:说明有部分课程(数量为numCourses - count)的入度始终无法变为0。这些课程之间存在循环依赖,导致无法开始学习,即图中存在环,返回false。
- 如果
为什么这样能找到环?
因为只有入度为0的节点才能被加入队列并处理。如果图中存在环,那么这个环上的所有节点都会互相依赖,它们的入度永远不可能被减到0(因为环内的节点彼此牵制,没有一个起点)。因此,这些节点永远不会被加入队列。最终,我们处理过的节点数 count 就会小于总节点数。
示例演练(以示例2为例):
numCourses = 2, prerequisites = [[1,0],[0,1]]
- 建图与计算入度:
- 对于
[1,0]:graph[0]增加1;inDegree[1]加1(变为1)。 - 对于
[0,1]:graph[1]增加0;inDegree[0]加1(变为1)。 graph[0] = [1],graph[1] = [0]inDegree = [1, 1]
- 对于
- 初始化队列: 寻找入度为0的节点。
inDegree[0]和inDegree[1]都是1,没有入度为0的节点。队列初始为空。 - BFS: 队列为空,直接结束。
- 判断结果:
count为0,不等于2。返回false。正确判断出存在环。
这个算法的时间复杂度是 O(V+E),其中 V 是课程数量(节点数),E 是先修条件数量(边数),非常高效。