(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,反之

相关推荐
CoovallyAIHub1 天前
YOLO-IOD深度解析:打破实时增量目标检测的三重知识冲突
深度学习·算法·计算机视觉
NAGNIP1 天前
轻松搞懂全连接神经网络结构!
人工智能·算法·面试
NAGNIP1 天前
一文搞懂激活函数!
算法·面试
董董灿是个攻城狮1 天前
AI 视觉连载7:传统 CV 之高斯滤波实战
算法
爱理财的程序媛2 天前
openclaw 盯盘实践
算法
MobotStone2 天前
Google发布Nano Banana 2:更快更便宜,图片生成能力全面升级
算法
颜酱2 天前
队列练习系列:从基础到进阶的完整实现
javascript·后端·算法
用户5757303346242 天前
两数之和:从 JSON 对象到 Map,大厂面试官到底在考察什么?
算法
程序猿追2 天前
“马”上行动:手把手教你基于灵珠平台打造春节“全能数字管家”
算法