207. 课程表

前言

「力扣题解」系列致力于分享有价值的题目、探讨更优秀的解法。

这是本系列的第 5 篇题解,更多题解关注 Somnia1337@力扣。

离散数学中学习了拓扑排序,本篇讲解其在实际问题中的应用。

题目描述

难度:🟡中等

标签:#深度优先搜索广度优先搜索拓扑排序

你必须选修 numCourses 门课程,记为 0 到 numCourses - 1

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

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

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

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

题意理解 & 解题思路

本题题解区有一篇高质量题解,我的水平不足以讲解得如此清晰,故直接搬运并小改如下:

作者:笨猪爆破组

链接:https://leetcode.cn/problems/course-schedule/

题意理解

一共有 n 门课,编号为 0 ~ n-1

先决条件 [b, a] 的意思是必须先上课 a、才能上课 b

给出 n 和一个先决条件表,判断能否完成所有课程。

用有向图描述先后关系

示例:n = 6,先决条件表:[[3, 0], [3, 1], [4, 1], [4, 2], [5, 3], [5, 4]]

0, 1, 2 没有先修课,可以直接选,其余的课都有 2 门先修课。

用有向图展现这种依赖关系:

这叫有向无环图,把一个有向无环图转成线性的排序就叫拓扑排序

有向图的入度出度:有向边 A B,给 A 增加了 1 个出度,给 B 增加了 1 个入度。

所以,节点 0、1、2 的入度为 0,节点 3、4、5 的入度为 2。

每次选当前可以上的课

每次只能选入度为 0 的课,因为它不依赖别的课,是当前能上的课。选择一门课后,依赖它的下一门课程的入度就减 1。

先选 0,课 3 的先修课就学完了一门,入度由 2 变 1。

再选 1,课 3 的入度变 0,课 4 的入度由 2 变 1。

再选 2,课 4 的入度变 0,现在,课 34 的入度为 0,都可以修读了。

持续选择入度为 0 的课,直到全部学完或选不到入度为 0 的课。

BFS

入度为 0 的课是当前能选的,入队。

逐个出队,代表着选择这门课,并将相关课程的入度减 1。

如果相关课的入度更新后变为 0,安排它入队,持续上述过程。

准备工作

每门课的入度需要被记录,以便后续更新。课之间的依赖关系也要记录,这样才知道修读当前课能减小哪些课的入度。

  • Map<Int, List<Int>>:记录每门课的后续课。
  • int[]:维护每门课的入度。
判断能否修完

count 记录出队的课程数,最后判断 count 是否等于总课程数。

复杂度

  • 时间复杂度: 为课程数, 为先决条件表长度
  • 空间复杂度:

代码

Java
class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
	    // follows: Map, 记录后续课程
	    // in: int[], 记录入度
        Map<Integer, List<Integer>> follows = new HashMap<>();
        int[] in = new int[numCourses];
        for (int[] edge : prerequisites) {
            List<Integer> next = follows.getOrDefault(edge[1], new ArrayList<>());
            next.add(edge[0]);
            in[edge[0]]++;
            follows.put(edge[1], next);
        }
        
        // take: Queue, 用于 BFS
        Queue<Integer> take = new LinkedList<>();
        // 把最初能选的课程入队
        for (int i = 0; i < numCourses; i++) {
            if (in[i] == 0) take.add(i);
        }
        
        // count: int, 记录修读的课程数
        int count = 0;
        // BFS
        while (!take.isEmpty()) {
            List<Integer> follow = follows.get(take.poll());
            count++;
            if (follow == null) continue;
            // 更新入度, 为 0 时入队
            for (int f : follow) {
                in[f]--;
                if (in[f] == 0) take.add(f);
            }
        }
        
        // 修读的课程数是否达到要求
        return count == numCourses;
    }
}

相关题目

210. 课程表 II

深度优先搜索广度优先搜索拓扑排序

630. 课程表 III

贪心数组排序(优先队列)

最后

更多系列「外源推文」&「生活分享」关注公众号: