(LeetCode-Hot100)207. 课程表

问题简介

207. 课程表 - LeetCode

复制代码
题解github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions

题目描述

你这个学期必须选修 numCourses 门课程,记为 0numCourses - 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
  • ai != bi
  • 所有 [ai, bi] 互不相同

解题思路

本题本质上是判断有向图中是否存在环。如果存在环,则无法完成所有课程(因为会出现循环依赖);否则可以完成。

我们可以将课程看作图中的节点,先修关系看作有向边(bi → ai 表示 biai 的先修课)。问题转化为:判断该有向图是否为有向无环图(DAG)

方法一:拓扑排序(Kahn 算法)

💡 核心思想:利用入度进行 BFS 拓扑排序。

  1. 构建邻接表和入度数组。
  2. 将所有入度为 0 的课程加入队列。
  3. 依次从队列中取出课程,将其指向的课程入度减 1;若某课程入度变为 0,则加入队列。
  4. 最终若处理的课程数等于 numCourses,说明无环,返回 true;否则存在环,返回 false

优点:直观、易于理解,同时可输出拓扑序列。

方法二:DFS + 三色标记法

💡 核心思想:通过 DFS 遍历检测回边(back edge)。

使用三种状态标记节点:

  • 0(未访问):尚未访问该节点。
  • 1(正在访问):当前 DFS 路径中正在访问该节点(即在递归栈中)。
  • 2(已完成):该节点及其所有后继已访问完毕,无环。

若在 DFS 过程中遇到状态为 1 的节点,说明存在环。

优点:空间效率略高,适合稀疏图。


代码实现

java 复制代码
// Java 实现

// 方法一:拓扑排序(Kahn 算法)
class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // 构建邻接表和入度数组
        List<List<Integer>> graph = new ArrayList<>();
        int[] indegree = new int[numCourses];
        
        for (int i = 0; i < numCourses; i++) {
            graph.add(new ArrayList<>());
        }
        
        for (int[] pre : prerequisites) {
            int from = pre[1], to = pre[0];
            graph.get(from).add(to);
            indegree[to]++;
        }
        
        // BFS
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (indegree[i] == 0) {
                queue.offer(i);
            }
        }
        
        int visited = 0;
        while (!queue.isEmpty()) {
            int course = queue.poll();
            visited++;
            for (int next : graph.get(course)) {
                indegree[next]--;
                if (indegree[next] == 0) {
                    queue.offer(next);
                }
            }
        }
        
        return visited == numCourses;
    }
}

// 方法二:DFS + 三色标记
class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            graph.add(new ArrayList<>());
        }
        for (int[] pre : prerequisites) {
            graph.get(pre[1]).add(pre[0]);
        }
        
        int[] color = new int[numCourses]; // 0: unvisited, 1: visiting, 2: visited
        
        for (int i = 0; i < numCourses; i++) {
            if (color[i] == 0 && hasCycle(graph, color, i)) {
                return false;
            }
        }
        return true;
    }
    
    private boolean hasCycle(List<List<Integer>> graph, int[] color, int node) {
        if (color[node] == 1) return true; // 发现环
        if (color[node] == 2) return false; // 已完成,无环
        
        color[node] = 1;
        for (int neighbor : graph.get(node)) {
            if (hasCycle(graph, color, neighbor)) {
                return true;
            }
        }
        color[node] = 2;
        return false;
    }
}
go 复制代码
// Go 实现

// 方法一:拓扑排序(Kahn 算法)
func canFinish(numCourses int, prerequisites [][]int) bool {
    graph := make([][]int, numCourses)
    indegree := make([]int, numCourses)
    
    for _, pre := range prerequisites {
        from, to := pre[1], pre[0]
        graph[from] = append(graph[from], to)
        indegree[to]++
    }
    
    queue := []int{}
    for i := 0; i < numCourses; i++ {
        if indegree[i] == 0 {
            queue = append(queue, i)
        }
    }
    
    visited := 0
    for len(queue) > 0 {
        course := queue[0]
        queue = queue[1:]
        visited++
        for _, next := range graph[course] {
            indegree[next]--
            if indegree[next] == 0 {
                queue = append(queue, next)
            }
        }
    }
    
    return visited == numCourses
}

// 方法二:DFS + 三色标记
func canFinish(numCourses int, prerequisites [][]int) bool {
    graph := make([][]int, numCourses)
    for _, pre := range prerequisites {
        graph[pre[1]] = append(graph[pre[1]], pre[0])
    }
    
    color := make([]int, numCourses) // 0: unvisited, 1: visiting, 2: visited
    
    var hasCycle func(int) bool
    hasCycle = func(node int) bool {
        if color[node] == 1 {
            return true
        }
        if color[node] == 2 {
            return false
        }
        
        color[node] = 1
        for _, neighbor := range graph[node] {
            if hasCycle(neighbor) {
                return true
            }
        }
        color[node] = 2
        return false
    }
    
    for i := 0; i < numCourses; i++ {
        if color[i] == 0 && hasCycle(i) {
            return false
        }
    }
    return true
}

示例演示

numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]] 为例:

复制代码
课程依赖图:
0 → 1 → 3
↓ ↗
2 ────┘
  • 拓扑排序过程
    • 初始入度:[0,1,1,2]
    • 入度为 0 的课程:0
    • 处理 012 入度减为 0 → 加入队列
    • 处理 13 入度减为 1
    • 处理 23 入度减为 0 → 加入队列
    • 处理 3
    • 共处理 4 门课程 ⇒ 返回 true

✅ 无环,可以完成。


答案有效性证明

  • 拓扑排序法 :若图中存在环,则环上所有节点入度 ≥1,永远不会被加入队列,最终 visited < numCourses
  • DFS 三色法:若存在环,则在 DFS 路径中必会再次访问到状态为"正在访问"的节点(即回边),从而检测到环。

两种方法均能充要地判断有向图是否存在环,因此解法正确。


复杂度分析

方法 时间复杂度 空间复杂度
拓扑排序(Kahn) O(V + E) O(V + E)
DFS 三色标记 O(V + E) O(V + E)

其中:

  • V = numCourses(顶点数)
  • E = prerequisites.length(边数)

📌 两种方法时间/空间复杂度相同,实际性能取决于图的结构和语言运行时。


问题总结

关键洞察:课程安排问题 ⇨ 有向图环检测 ⇨ 拓扑排序 / DFS 环检测。

适用场景

  • 拓扑排序:需输出合法顺序时优先使用。
  • DFS 三色法:仅需判断可行性时代码更简洁。

常见误区

  • 忽略图可能不连通(需遍历所有节点)。
  • 使用普通 visited 标记(无法区分"正在访问"和"已完成")。

💡 扩展思考

  • 若需输出任意一个合法课程顺序?→ 拓扑排序结果即为答案。
  • 若课程有学分、学期限制?→ 可结合动态规划或分层拓扑排序。

本题是图论基础应用的经典代表,掌握其解法对解决依赖关系类问题至关重要。

相关推荐
仰泳的熊猫2 小时前
题目1535:蓝桥杯算法提高VIP-最小乘积(提高型)
数据结构·c++·算法·蓝桥杯
那起舞的日子2 小时前
动态规划-Dynamic Programing-DP
算法·动态规划
yanghuashuiyue3 小时前
lambda+sealed+record
java·开发语言
闻缺陷则喜何志丹3 小时前
【前后缀分解】P9255 [PA 2022] Podwyżki|普及+
数据结构·c++·算法·前后缀分解
每天吃饭的羊3 小时前
时间复杂度
数据结构·算法·排序算法
盟接之桥3 小时前
盟接之桥EDI软件:API数据采集模块深度解析,打造企业数据协同新引擎
java·运维·服务器·网络·数据库·人工智能·制造
HoneyMoose3 小时前
Spring Boot 2.4 部署你的第一个 Spring Boot 应用需要的环境
java
皮皮林5514 小时前
为什么 Spring 和 IDEA 都不推荐使用 @Autowired 注解??
java
衍生星球4 小时前
【JSP程序设计】Servlet对象 — page对象
java·开发语言·servlet·jsp·jsp程序设计