把先修关系,看成有向图。
总共有 numCourses 门课程,编号从 0 到 numCourses - 1。数对 [a, b] 表示想学课程 a 必须先修完课程 b。题目问是否可能修完所有课程。
如果课程之间存在互相依赖(如 0 依赖 1,1 依赖 0),或者多门课串成一条闭环,就会出现“谁也无法先学”的死锁。这就是典型的有向图环检测。通过拓扑排序,如果能排出一个合法的先后学习线性序列,说明图是无环的(DAG);如果排不完,说明存在环依赖。
分批消解入度,解锁新课程。
选择情形并点击“下一步”,观察各节点入度如何随着前置课程的修完逐步扣减,以及入度降为 0 后如何入队推进。
- 正在修读
- 入度为 0 在等待
- 已修完
[]
0 / 0
1 / 1
初始状态:准备将初始入度为 0 的课程加入队列。
每修完一门课,它指向的后续课程入度减 1。只有入度归零,课程才能解锁。
把分批学课,交给队列。
- 01
建图方向是 b → a
数对
[a, b]代表先学b再学a。因此邻接表记录b的出边指向a,同时indegree[a]++记录a有一个未履行的前置。 - 02
只有入度减到 0 才能入队
一门课可能有 3 个前置。修完其中一个,只是入度从 3 变成 2;只有等全部 3 个前置都修完、入度真正变为 0 时,才能放入学习队列。
- 03
用 learned == numCourses 判环
如果图中有环,环上的节点由于互相等待,入度永远不可能降为 0。队列提前排空时,只要
learned < numCourses,就证明有环死锁。
第一批入队的是一开始就无要求的课;学完第一批,解锁出第二批;再学完第二批,解锁出第三批。这种按依赖层次层层推进的过程,本质上就是有向图上的 BFS。
入度数组与队列的紧凑配合。
这段代码来自仓库解法。使用静态邻接表建图并统计入度,队列保存当前可学课程,单循环即可完成拓扑推进与环检测。
/* 建图:b -> a,并统计入度 */
for (int i = 0; i < prerequisitesSize; i++) {
int a = prerequisites[i][0];
int b = prerequisites[i][1];
graph[b][graphSize[b]] = a;
graphSize[b]++;
indegree[a]++;
}
/* 初始所有入度为 0 的课入队 */
int queue[MAXN], front = 0, rear = 0;
for (int i = 0; i < numCourses; i++) {
if (indegree[i] == 0) {
queue[rear++] = i;
}
}
int learned = 0;
while (front < rear) {
int course = queue[front++];
learned++;
for (int i = 0; i < graphSize[course]; i++) {
int next = graph[course][i];
indegree[next]--;
if (indegree[next] == 0) {
queue[rear++] = next;
}
}
}
return learned == numCourses;仓库中使用固定数组模拟邻接表与队列,避免动态链表分配开销。对于超稀疏或极大规模图,亦可采用链式前向星压缩存储。
原复盘里,留下的四个提醒。
这四条真实记录在当时的题目笔记中。有向图的建图方向、入度递减时机与出入队概念,极易因为概念混淆而产生 Bug。
- 01 先修关系的建图方向别建反
[a, b]的含义是“先学b才能学a”,边应当是b -> a。建反会导致入度和出度的含义颠倒,永远排不出正确顺序。- 02 邻接表数组追加是赋值
- 写代码时注意
graph[b][graphSize[b]] = a;是单等号赋值,不要误敲成判断==,否则邻接边根本没有加进去。 - 03 队列里放的是“当前入度为 0”的课
- 队列绝不是用来存放所有课程的,只存放“前置要求已经被全部满足,现在立即可以修读”的课。
- 04 判环依据是 learned == numCourses
- 不要用队列是否为空来判断是否有环。队列总会变空,关键是队列变空时,真正学完的课程数
learned是否等于总课程数。
用几种典型形态,检查边界。
毫无先修限制
prerequisites = []所有课程初始入度均为 0,第一步全部入队,一轮顺次学完。
两门课互相死锁
[0, 1] 且 [1, 0]0 和 1 的入度都是 1,队列初始为空,直接返回 false。
部分课程在环中
0->3 正常,1<->2 互锁0 和 3 顺利学完,但 1 和 2 无法进入队列,最终 learned = 2 < 4 返回 false。
多起点并行展开
0 和 1 共同解锁 20 和 1 同时在第一批队列,待两者先后出队后,2 的入度彻底减为 0 顺利解锁。
从网格 BFS 到图论拓扑序。
从笔记 001 的网格 BFS,到笔记 004 的有向图拓扑排序,队列承担的核心角色是一脉相承的:每一轮只处理当前已经就绪的节点,在推进的同时顺水推舟地解锁下一轮。入度不仅是一个数字,更是依赖关系的计数器。
每个课程节点入队一次,每条先修边仅遍历一次。
邻接表、入度数组与队列的存储开销。
上述复杂度对应图论通用链表实现。仓库二维静态数组写法空间为 O(V²)。本页步进演示保留历史帧以支持回溯。
题目与原始记录
本篇根据公开刷题仓库里的实现、样例与错误记录整理,保留原有解法的关键细节。