当前位置: 首页 > news >正文

【LeetCode 热题 100】207. 课程表——DFS+三色标记

Problem: 207. 课程表

文章目录

  • 整体思路
  • 完整代码
  • 时空复杂度
    • 时间复杂度:O(V + E)
    • 空间复杂度:O(V + E)

整体思路

这段代码的目的是判断给定的课程和它们的先修课程要求是否能够构成一个可行的学习计划。这个问题在图论中等价于 检测一个有向图中是否存在环 (Cycle Detection in a Directed Graph)。如果存在环(例如,A->B->C->A),则意味着存在循环依赖,课程计划无法完成。

该算法采用了经典的 深度优先搜索(DFS) 配合 三色标记法 来高效地检测环。

  1. 构建图(邻接表)

    • 首先,代码将课程和先修关系转换成一个图的数据结构。课程是图的节点(Vertex),先修关系是图的有向边(Edge)。
    • List<Integer>[] g 是一个邻接表,g[i] 存储了所有以课程 i 为先修课的后续课程列表。例如,如果 [c1, c0] 是一个先修关系(要上c1必须先上c0),则会添加一条从 c0c1 的有向边,即 g[c0].add(c1)
  2. 三色标记法

    • 为了在DFS中检测环,算法使用了一个 colors 数组来标记每个节点(课程)的状态。每个节点可以有三种颜色:
      • 0 (白色):表示该节点尚未被访问。
      • 1 (灰色):表示该节点正在被访问。即它在当前的DFS递归调用栈中。
      • 2 (黑色):表示该节点及其所有后代节点都已经被访问完毕,并且从该节点出发没有发现环。
  3. 深度优先搜索 (DFS) 检测环

    • canFinish 方法是主驱动程序。它遍历所有课程,如果遇到一个尚未被访问过的节点(白色),就以它为起点开始一次DFS。这样做是为了确保图中所有不连通的部分都被检查到。
    • dfs(x, ...) 是核心的递归函数。当它访问一个节点 x 时:
      a. 首先将 x 标记为灰色 (colors[x] = 1),表示它进入了当前的递归路径。
      b. 然后遍历 x 的所有邻居节点 y(即所有以 x 为先修课的课程)。
      c. 对于每个邻居 y
      • 发现环:如果 y 的颜色是灰色 (colors[y] == 1),这意味着我们从 x 走到了一个已经在当前递归路径上的节点 y。这形成了一个反向边 (Back Edge),证明了图中存在一个环。此时,DFS函数立即返回 true(表示“找到了环”)。
      • 继续探索:如果 y 是白色 (colors[y] == 0),则对 y 进行递归调用 dfs(y, ...)。如果这个递归调用返回 true,说明在更深的路径中发现了环,需要将这个“找到环”的结果向上传递,所以也返回 true
    • 如果遍历完 x 的所有邻居都没有返回 true,说明从 x 出发的所有路径都是安全的(没有环)。此时,将 x 标记为黑色 (colors[x] = 2),表示该节点已经安全探索完毕,并返回 false(表示“没有找到环”)。
  4. 最终结果

    • canFinish 方法根据 dfs 的返回值来判断。如果任何一次 dfs 调用返回了 true(找到了环),canFinish 就立刻返回 false(课程无法完成)。
    • 如果主循环正常结束,意味着对所有节点的DFS都没有发现环,那么 canFinish 返回 true(课程可以完成)。

完整代码

class Solution {/*** 判断给定的课程和先修关系是否能完成所有课程。* @param numCourses 课程总数* @param prerequisites 先修关系数组,[course, prerequisite] 表示要上 course 必须先上 prerequisite* @return 如果可以完成所有课程则返回 true,否则返回 false*/public boolean canFinish(int numCourses, int[][] prerequisites) {// 步骤 1: 构建邻接表来表示课程依赖图// g[i] 存储了所有以课程 i 为先修课的后续课程列表List<Integer>[] g = new ArrayList[numCourses];Arrays.setAll(g, i -> new ArrayList<>()); // 初始化邻接表中的每个列表for (int[] p : prerequisites) {// p[1] -> p[0] 是一条有向边g[p[1]].add(p[0]);}// 步骤 2: 初始化颜色数组用于三色标记法// 0: white (未访问), 1: gray (访问中), 2: black (已访问完毕)int[] colors = new int[numCourses];// 步骤 3: 遍历所有课程节点// 必须遍历所有节点,以防图是不连通的for (int i = 0; i < numCourses; i++) {// 如果节点是白色的(未访问过),则从该节点开始进行深度优先搜索// dfs返回true表示“找到了环”if (colors[i] == 0 && dfs(i, g, colors)) {// 只要找到一个环,就说明课程无法完成,直接返回 falsereturn false;}}// 如果所有节点的DFS都没有发现环,说明课程可以完成return true;}/*** 深度优先搜索辅助函数,用于检测从节点 x 出发是否存在环。* @param x 当前访问的节点* @param g 邻接表* @param colors 颜色标记数组* @return 如果从 x 出发检测到环,则返回 true;否则返回 false。*/private boolean dfs(int x, List<Integer>[] g, int[] colors) {// 将当前节点 x 标记为灰色(访问中),表示它进入了当前递归路径colors[x] = 1;// 遍历 x 的所有邻居节点 yfor (int y : g[x]) {// 如果邻居 y 是灰色,说明我们遇到了一个反向边,即找到了一个环if (colors[y] == 1) {return true; // 发现环,返回 true}// 如果邻居 y 是白色(未访问),则对其进行递归DFS// 如果深层DFS返回true(发现了环),则将结果向上传递if (colors[y] == 0 && dfs(y, g, colors)) {return true; // 发现环,返回 true}// 如果邻居y是黑色,说明它已被安全探索过,无需处理。}// 如果从 x 出发的所有路径都探索完毕且没有发现环,// 则将 x 标记为黑色(已访问完毕)colors[x] = 2;// 从 x 出发没有发现环return false;}
}

时空复杂度

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

  1. 图的构建:构建邻接表需要遍历所有的先修关系 prerequisites 一次。如果先修关系的数量为 E,则这部分的时间复杂度是 O(E)
  2. DFS遍历
    • canFinish 方法中的主循环会尝试对每个顶点 i 调用 dfs,但由于有 colors[i] == 0 的判断,每个顶点作为DFS的起点最多只会被访问一次。
    • 在整个DFS过程中,每个顶点(Vertex)被访问一次(颜色从0变为1,再变为2),每条边(Edge)被遍历一次(在 for (int y : g[x]) 循环中)。
    • 因此,所有DFS调用的总时间复杂度与图的顶点数 V 和边数 E 之和成正比,即 O(V + E)

综合分析
总时间复杂度 = 图构建时间 + DFS时间 = O(E) + O(V + E) = O(V + E)
其中 VnumCoursesEprerequisites.length

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

  1. 邻接表 (g):这是主要的存储开销。邻接表需要存储所有的顶点和边。它包含一个大小为 V 的数组,并且所有列表中元素的总数等于 E。因此,邻接表的空间复杂度是 O(V + E)
  2. 颜色数组 (colors):需要一个大小为 V 的数组来存储每个顶点的颜色状态。空间复杂度为 O(V)
  3. 递归调用栈:在DFS过程中,递归的深度在最坏的情况下(例如一个长链条状的图)可以达到 V。因此,递归调用栈所需的空间复杂度是 O(V)

综合分析
总的空间复杂度由以上几部分相加决定:O(V + E) + O(V) + O(V)。在 Big O 表示法中,我们取最高阶项,所以最终的空间复杂度是 O(V + E)

参考灵神

http://www.dtcms.com/a/290750.html

相关文章:

  • 原型设计模式
  • [Plecs基础知识系列] 基于Plecs的半导体热仿真方法(实战篇)_建立热路模型与仿真
  • 多能量CT扫描性能模体的详细讲解
  • 小飞电视:开启智能娱乐新视界
  • 优思学院:精益制造的工具与方法有什么区别?
  • 2025三掌柜赠书活动第二十五期 网络安全应急响应实战
  • Web3.0 能为你带来哪些实质性的 改变与突破
  • Web LLM 安全剖析:以间接提示注入为核心的攻击案例与防御体系
  • 2025年终端安全管理系统的全方位解析,桌面管理软件的分析
  • 在 Windows上用WSL和VSCode进行Linux开发环境配置
  • 解决OpenHarmony中找不到pthread_cancel和pthread_setcanceltype等libc符号的问题
  • python学智能算法(二十七)|SVM-拉格朗日函数求解上
  • SVM(Support Vector Machine)从入门到精通
  • 部署zabbox企业级分布式监控
  • 软件卸载:金山毒霸怎么卸载?【图文讲解】【小白专属】
  • freertos双向链表的插入
  • AI知识点——MCP
  • 如何使用终端查看任意Ubuntu的版本信息
  • 使用Langchain调用模型上下文协议 (MCP)服务
  • 中文分词模拟器 - 华为OD统一考试(Java 题解)
  • CS231n-2017 Lecture5神经网络笔记
  • 【Java EE初阶 --- 网络原理】应用层---HTTP(HTTPS)协议
  • 内网与外网是通过什么进行传输的?内外网文件传输的安全方法
  • Java从入门到精通 - 面向对象高级(三)
  • kafka 生产和消费 性能测试工具 kafka-producer-perf-test.sh kafka-consumer-perf-test.sh
  • kafka 生产消息和消费消息 kafka-console-producer.sh kafka-console-consumer.sh
  • Python 进阶(六): Word 基本操作
  • ROS 与 Ubuntu 版本的对应关系
  • 初学者STM32—USART
  • 了解类加载器吗?类加载器的类型有哪些?