Java_拓扑排序_课程表(❗❗❗经典面试题❗❗❗)

262 阅读6分钟

题目链接:leetcode.cn/problems/co…

题目描述:

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

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

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

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

示例 1:

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

示例 2:

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

提示:

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • prerequisites[i] 中的所有课程对 互不相同

解析

先分析一下题目要求,你要修numCourses门课程,但是也给出了条件prerequisites[i] = [ai, bi](且prerequisites[i].length == 2),就是你想修 aia_i这门课之前得先把 bib_i给修完。但是这里prerequisites的长度是不固定的,如果numCourses的值比较大的话,条件的长度可能会很多。

简单来举例:你想吃饭,但你得有饭吃,所以你得先买饭或者自己做。如果你要自己做,那么你还得先买菜。

这题就是典型的拓扑排序,什么是拓扑排序?应该怎么解呢,其实和BFS差不读,而且还需要使用到BFS。我们先来看例子。

image.png

上图中我们给了 5 门课和 5 个条件,我们将这些条件画成图,这是一个单向无环图。从图直观看出,必须将 0 这门课修完才能修其他的课程。

因为 0的入度为 0,也就是没有任何箭头指向它,它不受任何限制。这里先回顾一下图的基本知识吧。

入度:简单来说就是有几个箭头指向你,比如 2的入度为 2,1的入度为 1,0的入度为 0;

出度:指向多少个元素,例如 0的出度为 2,4的出度为 0。

有环:通过箭头的指向可以形成环路,比如0指向2这里反过来的话,0、1、2就是一个环。那有环会造成一个什么效果呢?

如果这题给的条件中能形成环路的话,就得返回false,是无解的,比如0、1、2是一个环。你想修1,你得先修0,但是修0的话又得修2,修2的话还得先修1,破不了招啊!

那这题的解题思路就是先要将图表达出来,然后用度为0的这个条件来判断我们当前需要先修哪门,如果没有度为0的课了,就要判断是不是存在环,有环的话返回false。如果也没有环路的话,说明课程就都修完了,返回true;

所以拿例子来说的话:

先将0修完,然后1的入度就变为0了,2的入度变成了1;

1的入度为0,所以再修1,这样2的入度就为0,3的入度为1;

再修2,3的入度就变为0,4的入度为1;

3修完,最后再修入度变为0的4。

这就是一个拓扑排序的例子,就是先后问题。

(篇幅过长,重新再贴一张例题图方便观看)

image.png

那么代码我们怎么写呢,先解决第一个问题,如何用代码表示图。其实这里用到的是邻接表,不需要完整的将图连起来,只是相邻的、指向的元素给存到一起。比如: 0指向1和2,用邻接表存的话就是 0: 1、2;

1指向2和3 1:2、3;

2指向3 2:3;

3指向4 3:4;

4指向空 4:;

这样的结构我们应该很熟悉,就是哈希表,key为Integer类型,value可以为List类型。

第二个问题,每节课的入度怎么表达呢,题目中给的课程其实都是整数,所以我们可以用数组的下标当作课,也就是定义一个长度与numCourses相同的数组,然后下标就代表课程编号,入度的值就存在对应的arr[i]中。将以上工作做好后,其实就是我开头提到的BFS了(如果不知道的话可以看看之前的内容)。将入度为0的课程放到queue中,然后开始BFS,过程中将与被入度为0指向的课程的度减一,然后再判断其入度是否为0,是0的话添加到queue中,继续下一轮BFS。理解BFS的话这里的思路应该是很清晰的,不理解的话可以看之前的BFS算法题,当然看下面代码也是可以的。

整体的大概思路就是这样,我们开始编写代码!

代码

 class Solution {
     public boolean canFinish(int numCourses, int[][] prerequisites) {
        //获取prerequisites数组长度
        int preLength = prerequisites.length;
        //1.先画图(邻接表)
        Map<Integer, List<Integer>> map = new HashMap<>();
        int[] arr = new int[numCourses];
        for (int i = 0; i < preLength; i++) {
            //将数组中的值取出来,此时的 a 为 ai, b 为 bi,所以是先完成bi,再完成ai
            int a = prerequisites[i][0],b = prerequisites[i][1];
            //判断表中是否存在bi,如果不存在的话就创建一个,并且将ai放入其中,存在的话就直接放入
            if (!map.containsKey(b)) {
                //value的类型为List类型,这里创建为LinkedList和ArrayList都行
                map.put(b, new ArrayList<>());
            }
            //map.get(b)就是刚刚创建的数组,然后为它添加值
            //此处不好理解的话就看上面的邻接表,也就是0指向1、2,表达为 0:1、2
            map.get(b).add(a);
            
            //2.将所有课程的入度求出来
            //因为完成a之前必须完成b,所以这里a出现几次就是有几个b指向a,也就是几个入度
            arr[a]++;
        }
        
        //3.将入度为0的课程存放到queue中
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            //注意这里添加的是课程,不是入度的值,数组的下标代表的是课程
            if (arr[i] == 0) queue.add(i);
        }
        
        //4.开始BFS
        while (!queue.isEmpty()) {
            //取出入度为0的课程
            int cur = queue.poll();
            
            //将被cur指向的课程的入度减一,然后判断是否入度为0,为0的话添加到queue中
            /*这里要使用map.getOrDefault是因为如果key为是最后一个课程的话(例如例题中的4)
            value就是空的,所以要传一个空数组
            */
            for (int m : map.getOrDefault(cur,new ArrayList<>())) {
                //此处用的是增强for,map.getOrDefault(cur, ArrayList<>()))获取到的是数组,所以是遍历这个数组
                arr[m]--;
                //如果此课程的入度为0,添加到queue中
                if (arr[m] == 0) queue.add(m);
            }
            
        }
        //5.判断是否存在环
        //如果arr数组中还存在不为0的值,那么就存在环
        for (int m : arr) {
            if (m != 0) return false;
        }
        //6.返回true
         return true;
     }
 }