你这个学期必须选修 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 。这是不可能的。
提示:
1 <= numCourses <= 20000 <= prerequisites.length <= 5000prerequisites[i].length == 20 <= ai, bi < numCoursesprerequisites[i]中的所有课程对 互不相同
1. 算法核心思想
本段代码采用了一种基于拓扑排序(Topological Sort)的广度优先搜索(BFS)算法,常被称为 Kahn 算法。其核心目的是判断给定的课程先决条件是否构成一个有向无环图(DAG)。
解决本题的关键难点在于如何将线性的“先决条件对”转化为系统性的依赖关系网,并检测其中是否存在“死循环”(即互相依赖导致的死锁)。本算法通过入度统计与图的遍历策略成功规避了该问题:
- 状态记录(入度与邻接表):利用
inDegree数组精确记录每个节点的“被依赖次数”(即还差几门先修课),利用graph二维数组建立“解锁清单”(即当前课程学完后,能直接作为哪些后续课程的先决条件)。 - 状态剥离(BFS 层序推进):始终从入度为 0(绝对安全、无任何前置门槛)的节点开始处理。处理完一个节点,就等同于从图中“抹除”这个节点及其发出的边。通过不断剥离外围无依赖的节点,最终根据成功剥离的节点总数来判定图中是否存在环。
2. 代码逻辑拆解
-
第一阶段:数据结构初始化与建图 (Graph Construction) 利用
vector<int> inDegree和vector<vector<int>> graph分别作为入度表和邻接表。 通过单层for循环遍历prerequisites数组:inDegree[prerequisites[i][0]]++:统计目标课程所需的前置门槛数。graph[prerequisites[i][1]].push_back(prerequisites[i][0]):构建由“先修课”指向“后续课”的有向边。
-
第二阶段:源点收集 (Initial Queue Setup) 遍历所有课程节点
0到numCourses - 1。寻找所有inDegree[i] == 0的节点(即初始状态下毫无门槛的新手课程),将其全部推入queue中,作为后续 BFS 遍历的物理起点。 -
第三阶段:拓扑排序主循环 (BFS Traversal) 主控循环
while (!q.empty())模拟了依次学习可解锁课程的过程:- 节点出队与计数:取出队首课程
cur视为“已修读”,并将累计通关计数器count++。 - 依赖解除(传递效应):获取当前课程的解锁清单
temp = graph[cur],遍历该清单内的所有后续课程temp[i],将其对应的入度(门槛)减 1(inDegree[temp[i]]--)。 - 新源点入队:在门槛减 1 的瞬间进行判定,如果
inDegree[temp[i]] == 0,说明该后续课程的前置条件已全部满足,立刻将其推入队列q等待下一轮处理。
- 节点出队与计数:取出队首课程
-
第四阶段:结果判定 (Cycle Detection Validation) 循环结束后,检查已修读的课程总数
count是否与要求的总课程数numCourses相等。若相等,说明所有课程均成功解锁,图为无环图(返回true);若小于总数,说明剩下的课程因相互依赖形成死锁(有环),无法完成(返回false)。
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<int> inDegree(numCourses, 0);
vector<vector<int>> graph(numCourses);
queue<int> q;
for (int i = 0; i < prerequisites.size(); i++) {
inDegree[prerequisites[i][0]]++;
graph[prerequisites[i][1]].push_back(prerequisites[i][0]);
}
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) {
q.push(i);
}
}
int count = 0;
while (!q.empty()) {
int cur = q.front();
q.pop();
count++;
vector<int> temp = graph[cur];
for (int i = 0; i < temp.size(); i++) {
inDegree[temp[i]]--;
if (inDegree[temp[i]] == 0) {
q.push(temp[i]);
}
}
}
if (count == numCourses) {
return true;
}
return false;
}
};
3. 复杂度分析
-
时间复杂度:****
其中 代表课程总数(节点数
numCourses), 代表先决条件的总对数(边数prerequisites.size())。构建邻接表和入度数组需遍历所有边缘,耗时 ;寻找所有入度为 0 的节点需遍历所有顶点,耗时 。在 BFS 主循环中,每个节点最多入队、出队一次,其引出的每条边也只会被访问一次,耗时 。综上,整体时间复杂度为严格的线性的 。
-
空间复杂度:****
主要的内存开销由建图数据结构承担。邻接表
graph内部总共存储了 个元素(有向边);入度数组inDegree的大小为 。在最坏情况下(例如所有图节点互相完全独立,或呈单一发散的星型结构),队列q的峰值大小可能趋近于 。因此,整体内存消耗(空间复杂度)为 。
