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

相关推荐
草履虫建模12 小时前
力扣算法 1768. 交替合并字符串
java·开发语言·算法·leetcode·职场和发展·idea·基础
naruto_lnq14 小时前
分布式系统安全通信
开发语言·c++·算法
Jasmine_llq14 小时前
《P3157 [CQOI2011] 动态逆序对》
算法·cdq 分治·动态问题静态化+双向偏序统计·树状数组(高效统计元素大小关系·排序算法(预处理偏序和时间戳)·前缀和(合并单个贡献为总逆序对·动态问题静态化
爱吃rabbit的mq15 小时前
第09章:随机森林:集成学习的威力
算法·随机森林·集成学习
(❁´◡`❁)Jimmy(❁´◡`❁)16 小时前
Exgcd 学习笔记
笔记·学习·算法
YYuCChi16 小时前
代码随想录算法训练营第三十七天 | 52.携带研究材料(卡码网)、518.零钱兑换||、377.组合总和IV、57.爬楼梯(卡码网)
算法·动态规划
不能隔夜的咖喱16 小时前
牛客网刷题(2)
java·开发语言·算法
VT.馒头16 小时前
【力扣】2721. 并行执行异步函数
前端·javascript·算法·leetcode·typescript
进击的小头17 小时前
实战案例:51单片机低功耗场景下的简易滤波实现
c语言·单片机·算法·51单片机
咖丨喱18 小时前
IP校验和算法解析与实现
网络·tcp/ip·算法