【LeetCode 每日一题】207. 课程表

整体思路

1. 核心问题

题目要求判断是否所有课程都能完成。课程之间的先修关系构成了有向图

  • 如果图中存在(例如 A 是 B 的先修,B 又是 A 的先修),则产生死锁,无法完成所有课程。
  • 因此,问题转化为:检测有向图中是否存在环

2. 算法逻辑

代码使用了 DFS 进行遍历,并利用 flag 数组记录每个节点的状态(典型的三色标记法):

  • 0 (未访问):节点尚未被访问。
  • 1 (正在访问) :节点在当前的递归栈中。如果在 DFS 过程中遇到了状态为 1 的节点,说明绕了一圈又回到了当前路径上的节点,即发现了环
  • -1 (已访问/安全) :节点及其所有后代节点都已经检查过,且没有发现环。如果遇到状态为 -1 的节点,无需再次遍历,直接剪枝返回安全。

3. 具体步骤

  1. 建图 :使用邻接表 g 存储图结构。根据输入 prerequisites,如果 [0, 1] 表示修课程 0 必须先修 1,则建立一条边 1 -> 0
  2. 全图遍历 :因为图可能是不连通的(包含多个独立的连通分量),所以需要对外层所有节点 0numCourses-1 进行循环,确保每个节点都被检查到。
  3. DFS 递归
    • 进入节点 i,标记为 1(正在访问)。
    • 遍历 i 的所有邻居(后续课程)。
    • 如果邻居返回 false(发现环),则层层向上返回 false
    • 所有邻居遍历完且无环,将节点 i 标记为 -1(安全),并返回 true

完整代码

java 复制代码
import java.util.ArrayList;
import java.util.List;

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // 1. 构建邻接表 (Adjacency List) 来表示有向图
        // 索引 i 代表先修课程,g.get(i) 存储依赖于 i 的后续课程列表
        List<List<Integer>> g = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            g.add(new ArrayList<>());
        }
        
        // 填充邻接表
        // input: [0, 1] 表示修课程 0 需先修 1,即边为 1 -> 0
        for (int[] e : prerequisites) {
            g.get(e[1]).add(e[0]);
        }
        
        // flag 数组用于记录每个课程节点的访问状态 (三色标记法)
        // 0: 未访问 (Unvisited)
        // 1: 正在访问 (Visiting, 当前递归栈中)
        // -1: 已访问且安全 (Visited, 确定无环)
        int[] flag = new int[numCourses];
        
        // 2. 遍历所有课程,处理图可能不连通的情况
        for (int i = 0; i < numCourses; i++) {
            // 对每个未完全确认安全的节点进行 DFS
            // 如果 dfs 返回 false,说明检测到了环,直接返回 false
            if (!dfs(g, flag, i)) {
                return false;
            }
        }
        
        // 如果所有节点都检查完毕且没有发现环,返回 true
        return true;
    }

    // 深度优先搜索辅助函数
    // 返回 true 表示从节点 i 出发没有环,返回 false 表示有环
    private boolean dfs(List<List<Integer>> g, int[] flag, int i) {
        // 情况 1: 当前节点标记为 1,说明在当前递归路径中再次遇到了它 -> 发现环
        if (flag[i] == 1) return false;
        
        // 情况 2: 当前节点标记为 -1,说明之前已经检查过该节点及其后续路径,是安全的 -> 剪枝
        if (flag[i] == -1) return true;
        
        // 情况 3: 当前节点为 0 (未访问),开始访问
        // 将状态标记为 1 (正在访问)
        flag[i] = 1;
        
        // 递归访问当前课程的所有后续课程 (邻居)
        for (Integer e : g.get(i)) {
            // 如果后续路径中发现环,直接向上返回 false
            if (!dfs(g, flag, e)) {
                return false;
            }
        }
        
        // 所有邻居都检查完毕,没有发现环
        // 将状态标记为 -1 (已访问且安全),避免重复计算
        flag[i] = -1;
        
        return true;
    }
}

时空复杂度

假设课程数量(节点数)为 VVV(即 numCourses),先修关系数量(边数)为 EEE(即 prerequisites.length)。

1. 时间复杂度:O(V+E)O(V + E)O(V+E)

  • 建图 :遍历所有边,耗时 O(E)O(E)O(E);初始化邻接表耗时 O(V)O(V)O(V)。
  • DFS 遍历
    • 由于使用了 flag 数组进行记忆化(剪枝),每个节点最多被访问一次(从状态 0 变为 1,再变为 -1)。
    • 在 DFS 过程中,每条边也最多被检查一次。
    • 因此 DFS 的总时间复杂度是 O(V+E)O(V + E)O(V+E)。
  • 总计 :O(V+E)O(V + E)O(V+E)。

2. 空间复杂度:O(V+E)O(V + E)O(V+E)

  • 邻接表 g :需要存储 VVV 个列表头和 EEE 条边,空间为 O(V+E)O(V + E)O(V+E)。
  • 标记数组 flag :长度为 VVV,空间为 O(V)O(V)O(V)。
  • 递归栈 :在最坏情况下(例如图退化为一条长链),递归深度可达 VVV,空间为 O(V)O(V)O(V)。
  • 总计 :O(V+E)O(V + E)O(V+E)。
相关推荐
寻寻觅觅☆6 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
偷吃的耗子7 小时前
【CNN算法理解】:三、AlexNet 训练模块(附代码)
深度学习·算法·cnn
化学在逃硬闯CS7 小时前
Leetcode1382. 将二叉搜索树变平衡
数据结构·算法
ceclar1238 小时前
C++使用format
开发语言·c++·算法
Gofarlic_OMS8 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
夏鹏今天学习了吗8 小时前
【LeetCode热题100(100/100)】数据流的中位数
算法·leetcode·职场和发展
忙什么果9 小时前
上位机、下位机、FPGA、算法放在哪层合适?
算法·fpga开发
董董灿是个攻城狮9 小时前
AI 视觉连载4:YUV 的图像表示
算法
ArturiaZ10 小时前
【day24】
c++·算法·图论
大江东去浪淘尽千古风流人物10 小时前
【SLAM】Hydra-Foundations 层次化空间感知:机器人如何像人类一样理解3D环境
深度学习·算法·3d·机器人·概率论·slam