1462. 课程表 IV

难度: 中等
来源: 每日一题 2023.09.12

现你总共需要上 numCourses 门课,课程编号依次为 0numCourses-1 。你会得到一个数组 prerequisite ,其中 prerequisites[i] = [ai, bi] 表示如果你想选 bi 课程,你 必须 先选 ai 课程。

  • 有的课会有直接的先修课程,比如如果想上课程 1 ,你必须先上课程 0 ,那么会以 [0,1] 数对的形式给出先修课程数对。

先决条件也可以是 间接 的。如果课程 a 是课程 b 的先决条件,课程 b 是课程 c 的先决条件,那么课程 a 就是课程 c 的先决条件。

你也得到一个数组 queries ,其中 queries[j] = [uj, vj]。对于第 j 个查询,您应该回答课程 uj 是否是课程 vj 的先决条件。

返回一个布尔数组 answer ,其中 answer[j] 是第 j 个查询的答案。

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]], queries = [[0,1],[1,0]]
输出:[false,true]
解释:课程 0 不是课程 1 的先修课程,但课程 1 是课程 0 的先修课程。

示例 2:

输入:numCourses = 2, prerequisites = [], queries = [[1,0],[0,1]]
输出:[false,false]
解释:没有先修课程对,所以每门课程之间是独立的。

示例 3:

输入:numCourses = 3, prerequisites = [[1,2],[1,0],[2,0]], queries = [[1,0],[1,2]]
输出:[true,true]

提示:

  • 2 <= numCourses <= 100
  • 0 <= prerequisites.length <= (numCourses * (numCourses - 1) / 2)
  • prerequisites[i].length == 2
  • 0 <= ai, bi <= n - 1
  • ai != bi
  • 每一对 [ai, bi] 都 不同
  • 先修课程图中没有环。
  • 1 <= queries.length <= 104
  • 0 <= ui, vi <= n - 1
  • ui != vi
class Solution {
    public List<Boolean> checkIfPrerequisite(int numCourses, int[][] prerequisites, int[][] queries) {

    }
}

分析与题解


  • 深度优先遍历 + 邻接表 + 暴力搜寻

    已失败, 原因: 暴力搜索时间过长

    虽然失败了, 但是也是我立马想到的方案, 还是利用邻接表的形式, 对于queries中每一组 都进行一次完成的深度优先搜索, 所以会有很多的重复过程, 然后最终导致时间超时.

    整体的解题思路还是和前面的题目一样, 还是先构建邻接表, 然后进行深度优先遍历.

    // 构建邻接表
    Map<Integer, List<Integer>> cache = new HashMap<>();
    for(int[] item : prerequisites) {
        if(cache.get(item[0]) == null) {
            cache.put(item[0], new ArrayList<>());
        }
        cache.get(item[0]).add(item[1]);
    }
    

    在这种考虑情况下, 有人会说为啥不能把先前的遍历过程中已知的关系存储起来呢? 其实我当时也是在想这样的问题, 我该如何解决, 但是当时没有想到合适的解决方案, 所以就直接采用了暴力搜寻的方案了.

    List<Boolean> result = new ArrayList<>();
    for(int[] item : queries) {
        result.add(dfs(cache, cache.get(item[0]), item[1]));
    }   
    

    对于深度遍历方法的实现, 当我们遇到没有邻接表的时候,就直接返回false, 认为两个课程直接没有任何关系.

    其他按照正常情况寻找即可. 还是那话, 暴力搜寻很简单, 但是容易时间超时.

    public Boolean dfs(Map<Integer, List<Integer>> cache, List<Integer> subCache, int find) {
        if (subCache == null) {
            return false;
        }
        for (Integer item : subCache) {
            if (item == find) {
                return true;
            }
            if (dfs(cache, cache.get(item), find)) {
                return true;
            }
        }
        return false;
    }
    

    接下来,我们看一下整体的解题思路如下所示.

    class Solution {
        public List<Boolean> checkIfPrerequisite(int numCourses, int[][] prerequisites, int[][] queries) {
            // 构建邻接表
            Map<Integer, List<Integer>> cache = new HashMap<>();
            for(int[] item : prerequisites) {
                if(cache.get(item[0]) == null) {
                    cache.put(item[0], new ArrayList<>());
                }
                cache.get(item[0]).add(item[1]);
            }
            List<Boolean> result = new ArrayList<>();
            for(int[] item : queries) {
                result.add(dfs(cache, cache.get(item[0]), item[1]));
            }        
            return result;
        }
        public Boolean dfs(Map<Integer, List<Integer>> cache, List<Integer> subCache, int find) {
            if (subCache == null) {
                return false;
            }
            for (Integer item : subCache) {
                if (item == find) {
                    return true;
                }
                if (dfs(cache, cache.get(item), find)) {
                    return true;
                }
            }
            return false;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O(n²), 每一个节点都最多要搜寻一遍邻接表内容.
    • 空间复杂度: O(n), 会创建两个空间 resultcache , 每一个空间都与所有课程节点数成线性关系.

    结果如下所示.


  • 深度优先遍历 + 优先矩阵

    通过上面的失败解题经验, 我们知道, 我们必须要找到一个合适的方式来存储 课程 i 和 所有课程的关系, 这时候我们就必须要构建一个 关系矩阵 . 我们创建一个 numCourses x numCourses 的矩阵. 用来存储每一个课程和任意课程的关系.

    然后我们通过深度优先遍历往矩阵中填充 truefalse, 这时候有一个问题, 我们如何知道 课程 A课程 B间接先导课程 呢?

    直接先导课程好说, 我们通过邻接表就可以获得, 那 间接先导课程 呢?

    这里就要说一点, 那就是通过邻接节点来解决这个问题, 在递归过程中, 我们一定会先搞定邻接节点的关系表, 假设如图所示.

    在上图中, 假设我们查找课程为 1 , 查找到邻接节点 2 中。 我们会找到2中的邻接关系, 如果 12 有直接的邻接关系, 那么 1 和 2 中的 所有邻接节点 都有先导关系.

    找到这样的逻辑之后, 为了防止重复多次遍历, 我们需要一个数组来记录已经查找过的点(这个点以及查找了所有的邻接关系 X轴方向)

    然后我们看一下我们的解题过程.

    依然是, 先构建邻接表.

    // 构建邻接表
    Map<Integer, List<Integer>> cache = new HashMap<>();
    for(int[] item : prerequisites) {
        if(cache.get(item[0]) == null) {
            cache.put(item[0], new ArrayList<>());
        }
        cache.get(item[0]).add(item[1]);
    }
    

    然后就是构建矩阵 isPre 和 已经遍历过的节点数组 vi

    boolean[] vi = new boolean[numCourses];
    boolean[][] isPre = new boolean[numCourses][numCourses];
    

    然后就是对每一个课程都进行深度优先遍历.

    for(int i = 0; i < numCourses; i++) {
        dfs(cache, isPre, vi, i);
    }
    

    在深度优先遍历方法中, 我们首先要记录是否被遍历过, 如果遍历过, 那就不遍历这个节点了, 也是为了防止存在环的情况.

    // 判断先前是否遍历过,这个return包含两个意义.
    // 1. 判断本次是否遍历过,防止有环
    // 2. 判断别的邻接表条目是否遍历过, 防止过度遍历
    if (vi[index]) {
        return;
    }
    

    然后把当前节点添加到已经查找完成的数组中.

    vi[index] = true;
    

    然后就是对当前节点的所有邻接节点进行遍历.

    for (int item : list) {
        // 添加直接关系
        isPre[index][item] = true;
        dfs(cache, isPre, vi, item);
        // 添加间接关系
        for (int i = 0; i < isPre.length; ++i) {
            isPre[index][i] = isPre[index][i] | isPre[item][i];
        }
    }
    

    上面的代码我们就可以如何添加的间接关系, 是通过 isPre[index][i] = isPre[index][i] | isPre[item][i] 来判断的.

    这样, 整体的解题过程就完成了.


    接下来, 我们一起看下完整的解题过程.

    class Solution {
        public List<Boolean> checkIfPrerequisite(int numCourses, int[][] prerequisites, int[][] queries) {
            // 构建邻接表
            Map<Integer, List<Integer>> cache = new HashMap<>();
            for(int[] item : prerequisites) {
                if(cache.get(item[0]) == null) {
                    cache.put(item[0], new ArrayList<>());
                }
                cache.get(item[0]).add(item[1]);
            }
            boolean[] vi = new boolean[numCourses];
            boolean[][] isPre = new boolean[numCourses][numCourses];
            for(int i = 0; i < numCourses; i++) {
                dfs(cache, isPre, vi, i);
            }
            List<Boolean> result = new ArrayList<>();
            for(int[] item : queries) {
                result.add(isPre[item[0]][item[1]]);
            }
            return result;
        }
        public void dfs(Map<Integer, List<Integer>> cache, boolean[][] isPre, boolean[] vi, int index) {
            // 判断先前是否遍历过,这个return包含两个意义.
            // 1. 判断本次是否遍历过,防止有环
            // 2. 判断别的邻接表条目是否遍历过, 防止过度遍历
            if (vi[index]) {
                return;
            }
            vi[index] = true;
            List<Integer> list = cache.get(index);
            if (list != null) {
                for (int item : list) {
                    // 添加直接关系
                    isPre[index][item] = true;
                    dfs(cache, isPre, vi, item);
                    // 添加间接关系
                    for (int i = 0; i < isPre.length; ++i) {
                        isPre[index][i] = isPre[index][i] | isPre[item][i];
                    }
                }
            }
        }
    }
    

    复杂度分析:

    • 时间复杂度: O(numCourses² + m + n)
    • 空间复杂度: O(numCourses² + n),其中 numCourses 为课程数,n 为题目给出的先决条件的数目,主要为构建有向图和矩阵 isPre 的空间开销。注意返回值不计入空间复杂度。

    结果如下所示.


IT界无底坑洞栋主 欢迎加Q骚扰:676758285