LeetCode 207. 课程表

题目描述

这个学期需要选修 numCourses 门课程,课程编号为 0 到 numCourses - 1。

数组 prerequisites 表示课程之间的先修关系,其中 prerequisites[i] = [ai, bi] 表示:如果要学习课程 ai,必须先学习课程 bi。

例如:

复制代码
[0, 1]

表示想学习课程 0,需要先完成课程 1。

要求判断是否可以完成所有课程。如果可以,返回 true;否则返回 false。

初始思路

一开始想到的是把二维数组转成链表,然后判断链表里是否存在循环。

但这题不是普通链表问题。一个课程可能有多个后续课程,也可能被多个课程依赖,所以它本质上是一张 有向图,不是一条链表。

如果课程之间存在环,就说明有些课程互相依赖,无法完成所有课程。

例如:

复制代码
0 -> 1
1 -> 0

这就表示学习 0 之前要先学 1,学习 1 之前又要先学 0,形成了死循环。

解题思路

这题可以用 DFS 判断有向图中是否存在环。

先根据 prerequisites 建图:

复制代码
g[p[1]].add(p[0]);

这里的方向是:

复制代码
先修课 -> 后续课

也就是如果 p = [a, b],表示学 a 之前必须先学 b,所以建边 b -> a。

然后用三色标记记录每个课程的访问状态:

  • 0:未访问。
  • 1:正在访问。
  • 2:已经访问完成。

DFS 过程中,如果遇到一个状态为 1 的节点,说明这个节点还在当前递归路径上,又被重新访问到了,所以存在环。

如果存在环,就无法完成所有课程,返回 false。

为什么需要三色标记

这题不能只用一个简单的 visited。

因为有向图判环时,需要区分两种状态:

  • 这个点以前访问过,并且已经确认它后面的路径没有环。
  • 这个点正在当前 DFS 路径中,还没有退出递归。

只有遇到"正在访问"的点,才说明形成了环。

也就是代码里的:

复制代码
if (color[y] == 1) {
    return true;
}

当一个节点的所有后续节点都 DFS 完成后,要把它标记成 2:

复制代码
color[x] = 2;

表示这个点已经检查完成,以后再遇到它就不用重复搜索。

易错点

1. 建图方向

prerequisites[i] = [ai, bi] 的含义是:学 ai 之前要先学 bi。

所以边的方向应该是:

复制代码
bi -> ai

对应代码:

复制代码
g[p[1]].add(p[0]);

2. DFS 结束后要标记为完成

如果一个点搜索完没有发现环,要把它从 1 改成 2。

否则其他路径再次访问到它时,可能会误以为遇到了环。

3. 外层要遍历所有课程

图不一定是连通的。

有些课程可能和课程 0 完全不在一个连通块里,所以不能只从一个课程开始 DFS,而是要遍历所有课程:

复制代码
for (int i = 0; i < numCourses; i++) {
    if (color[i] == 0 && dfs(i, g, color)) {
        return false;
    }
}

代码实现

复制代码
class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        List<Integer>[] g = new ArrayList[numCourses];
        Arrays.setAll(g, i -> new ArrayList<>());
        int[] color = new int[numCourses];

        for (int[] p : prerequisites) {
            g[p[1]].add(p[0]);
        }

        for (int i = 0; i < numCourses; i++) {
            if (color[i] == 0 && dfs(i, g, color)) {
                return false;
            }
        }

        return true;
    }

    public boolean dfs(int x, List<Integer>[] g, int[] color) {
        color[x] = 1;

        for (int y : g[x]) {
            if (color[y] == 1 || color[y] == 0 && dfs(y, g, color)) {
                return true;
            }
        }

        color[x] = 2;
        return false;
    }
}

复杂度分析

  • 时间复杂度:O(numCourses + prerequisites.length)。每个课程节点和每条先修边最多被访问一次。
  • 空间复杂度:O(numCourses + prerequisites.length)。邻接表需要存储所有边,递归栈和颜色数组最多需要 O(numCourses)。

复盘

这题的关键是把课程关系看成一张有向图,然后判断图里有没有环。

最开始想用链表判环,是因为抓住了"循环依赖"这个方向,但没有意识到课程关系不是一条链,而是可能一对多、多对一的图结构。

下次遇到类似题时,可以先检查三点:

  • 依赖关系能不能抽象成有向图。
  • 边方向是否是 先修课 -> 后续课。
  • DFS 判环时是否区分了 未访问、正在访问、已完成 三种状态。
相关推荐
布莱克60514 小时前
C++ 中 map 的 insert 与 emplace:key 已存在时会覆盖吗?
开发语言·数据结构·c++·算法·map
D e re k ZH17 小时前
C++ 系统学习手记:从语法细节到工程实践
开发语言·数据结构·c++·算法
石小石Orz18 小时前
AI 正在蚕食我们的编码能力
前端·后端·算法
民乐团扒谱机19 小时前
【微实验】NSGA-II(非支配排序遗传算法)——解决多目标优化问题的“自然选择模拟器”
linux·人工智能·算法
青少儿编程课堂19 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:中缀表达式求值解析
c++·python·算法·bfs·信息学竞赛
武子康21 小时前
Qwen-Drive 到底是什么:4B 级 VLM 如何成为驾驶策略底座
人工智能·算法·机器学习
zh_xuan1 天前
LeeCode 496. 下一个更大元素I
数据结构·算法·栈·单调栈
机 _ 长1 天前
05 智算运维|8 张卡怎么才算「捆成一张」:NCCL 实测与拓扑
运维·人工智能·算法·智算运维
早起CaiCai1 天前
【2023-01 OM】利用深度学习算法融合异质观测实现海洋场降尺度
人工智能·深度学习·算法