(leetcode)力扣100 53课程表(深搜+拓扑排序)

题目

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。

例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

数据范围

1 <= numCourses <= 2000

0 <= prerequisites.length <= 5000

prerequisites[i].length == 2

0 <= ai, bi < numCourses

prerequisites[i]中的所有课程对 互不相同

测试用例

示例1

java 复制代码
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例2

java 复制代码
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

题解

java 复制代码
import java.util.Arrays;

class Solution {
    // idx 用于给每一条边分配唯一的索引(相当于链表节点的内存地址)
    int idx = 0;
    
    // N 是数组容量。注意:这里包含了节点头数组 h 和边数组 e, ne。
    // 警告:如果题目中 prerequisites 的长度(边数)超过 5000,e 和 ne 数组会越界。
    // 建议:e 和 ne 的大小最好根据 prerequisites.length 来初始化,或者开得比 N 大很多。
    int N = 5001; 
    
    // h[u] 存储节点 u 的第一条边的索引(Head),初始化为 -1 表示没有边
    int h[] = new int[N];
    
    // e[i] 存储第 i 条边指向的目标节点(Edge/End),相当于链表节点的 value
    int e[] = new int[N];
    
    // ne[i] 存储第 i 条边的下一条同起点边的索引(Next Edge),相当于链表节点的 next 指针
    int ne[] = new int[N];
    
    // valid 标志位,一旦发现环,设为 false
    boolean valid = true;
    
    // visited 数组用于三色标记法:
    // 0: 未访问 (Unvisited)
    // 1: 正在访问 (Visiting) - 当前递归栈中,如果再次遇到说明有环
    // 2: 已完成 (Visited) - 该节点及其子节点都已检查无环
    int visited[];

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // 初始化头节点数组为 -1
        Arrays.fill(h, -1);
        visited = new int[numCourses];
        
        // 建图:遍历先修课程数组
        // info[0] 是课程,info[1] 是先修课。也就是 info[1] -> info[0]
        for(int[] info : prerequisites){
            add(info[1], info[0]);
        }
        
        // 遍历所有课程,处理非连通图的情况
        // 只要 valid 为 true 且还有未访问节点,就继续 DFS
        for(int i = 0; i < numCourses && valid; i++){
            if(visited[i] == 0){
                dfs(i);
            }
        }

        return valid;
    }

    public void dfs(int u){
        // 标记当前节点 u 为"正在访问"(入栈)
        visited[u] = 1;
        
        // 链式前向星遍历:
        // i = h[u]: 获取 u 的第一条边的索引
        // i != -1: 还有边没遍历完
        // i = ne[i]: 跳到下一条边的索引
        for(int i = h[u]; i != -1; i = ne[i]){
            int j = e[i]; // 【关键】取出当前边 i 指向的邻居节点 j
            
            if(visited[j] == 0){
                // 如果邻居 j 没访问过,递归访问
                dfs(j);
                // 如果递归回来发现已经有环了,直接返回,剪枝
                if(!valid){
                    return;
                }    
            } else if(visited[j] == 1){
                // 如果邻居 j 状态是 1,说明 j 还在当前的递归栈中
                // 这意味着我们从 u 指回了它的祖先 j,发现了环!
                valid = false;
                return;
            }
            // 如果 visited[j] == 2,说明该节点安全,直接跳过
        }
        
        // 当前节点 u 的所有邻居都遍历完了,且没有发现环
        // 标记为"已完成"(出栈)
        visited[u] = 2;
    }

    // 添加边的函数(头插法)
    // a -> b (a 指向 b)
    public void add(int a, int b){
        e[idx] = b;      // 1. 记录第 idx 条边的终点是 b
        ne[idx] = h[a];  // 2. 第 idx 条边的 next 指向节点 a 原来的第一条边
        h[a] = idx++;    // 3. 更新节点 a 的第一条边为当前边 idx,并将 idx 加 1
    }
}

思路

这道题难点在于知道他想考什么,什么情况下选课会失败,就是当课程的前置要求之间形成了一个环。对于前置要求,我们使用拓扑排序可以满足其性质,我们在实现拓扑排序的图中,记录并判断是否存在环即可。存在无环拓扑排序即满足题目要求返回true,反之

相关推荐
范纹杉想快点毕业2 小时前
嵌入式通信协议深度解析:从SPI/I2C到CAN总线的完整实现指南嵌入式工程师的炼成之路:从校园到实战的跨越
linux·运维·服务器·数据库·算法
啊阿狸不会拉杆2 小时前
《数字信号处理》第10章-数字信号处理中的有限字长效应
算法·matlab·fpga开发·信号处理·数字信号处理·dsp
week_泽2 小时前
GBDT 算法中构建第一个弱学习器(CART 回归树)-计算示例
学习·算法·回归·gbdt
傻小胖2 小时前
16.ETH-状态树-北大肖臻老师客堂笔记
笔记·算法·区块链·哈希算法
张张努力变强2 小时前
C++ 类和对象(五):初始化列表、static、友元、内部类等7大知识点全攻略
开发语言·数据结构·c++·算法
老鼠只爱大米2 小时前
LeetCode经典算法面试题 #23:合并K个升序链表(分支法、优先队列等多种实现方案详细解析)
算法·leetcode·链表·优先队列·多路归并·分治法·合并链表
啵啵鱼爱吃小猫咪2 小时前
机器人几何雅可比与解析雅可比
人工智能·学习·算法·机器学习·matlab·机器人
养军博客2 小时前
C语言五天速成(可用于蓝桥杯备考)
c语言·数据结构·算法
zhangkaixuan4562 小时前
Paimon Split 机制深度解析
java·算法·数据湖·lsm-tree·paimon