XingHuiSamaの宝藏之地
首页项目归档照片墙音乐灵境说说杂谈友链关于
封面

Leetcode一百题——课程表

2026-05-08 13:53:42
# Leetcode
# C++
# 题解
# 工作

你这个学期必须选修 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 <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • prerequisites[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. 复杂度分析

  • 时间复杂度:****O(V+E)O(V + E)O(V+E)

    其中 VVV 代表课程总数(节点数 numCourses),EEE 代表先决条件的总对数(边数 prerequisites.size())。

    构建邻接表和入度数组需遍历所有边缘,耗时 O(E)O(E)O(E);寻找所有入度为 0 的节点需遍历所有顶点,耗时 O(V)O(V)O(V)。在 BFS 主循环中,每个节点最多入队、出队一次,其引出的每条边也只会被访问一次,耗时 O(V+E)O(V + E)O(V+E)。综上,整体时间复杂度为严格的线性的 O(V+E)O(V + E)O(V+E)。

  • 空间复杂度:****O(V+E)O(V + E)O(V+E)

    主要的内存开销由建图数据结构承担。邻接表 graph 内部总共存储了 EEE 个元素(有向边);入度数组 inDegree 的大小为 VVV。在最坏情况下(例如所有图节点互相完全独立,或呈单一发散的星型结构),队列 q 的峰值大小可能趋近于 VVV。因此,整体内存消耗(空间复杂度)为 O(V+E)O(V + E)O(V+E)。

‍

avatar

XingHuiSama

在代码、学术与分子动力学模拟间穿梭的普通人。近期正埋头于 GROMACS 模拟研究与神经网络计算。

RECOMMENDED

GROMACS 2025 分子动力学模拟初探2222

2026-03-24 07:00:45

Computational Chemistry Tool 工具二介绍

2026-04-01 07:00:23

Leetcode一百题——字母异位词分组

2026-04-07 15:34:18

Table of Contents