课程表 II-- 拓扑排序思想

0x01.问题

现在你总共有 n 门课需要选,记为 0n-1

在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]

给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。

可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。

示例 1:

输入: 2, [[1,0]] 输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。
因此,正确的课程顺序为
[0,1] 。

示例 2:

输入: 4, [[1,0],[2,0],[3,1],[3,2]] 输出: [0,1,2,3] or [0,2,1,3] 解释: 总共有 4
门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。

说明:

  • 输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
  • 你可以假定输入的先决条件中没有重复的边。

提示:

  • 这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
  • 通过 DFS 进行拓扑排序 -
  • 拓扑排序也可以通过 BFS 完成。

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/course-schedule-ii

0x02.一步步分析思路

  • 读题,获取一些有效信息:

    • 围绕的是0n-1的这n门课程的选取顺序问题。
    • 第二个参数代表着这些课程之间的依赖关系,即[0,1]表示要学0,先学1。
    • 结果需要返回选取课程的顺序,如果不能全部选取,返回空数组。
  • 一步步来解决问题,第一步:什么样的课程才能被依次选取?

    • 换句话说,在没有选任何课程的时候,我们能够选什么课,在选了一些课程之后,又能够选什么课?
    • 在没有选任何课时,很简单,所有不依赖其它课的课都可以选,这在这个依赖关系图中也就是入度为0的情况
    • 在选了一些课程的情况下,有些复杂了,但总体还是:可以选依赖已经选过的课的课程。
    • 综合一下上述两种情况,我们发现,似乎总在提醒着我们什么,综合两种情况,最重要的就是一个节点的入度情况,也就是说,入度为0就可以选了,依赖的课每选一门,则相应的入度减1,这两种情况也就能统一化了。
    • 所以,第一步就是要维护一个存储节点入度情况的数组courseLib.
  • 第二步,如何继续对第一步的想法继续完善?

    • 维护了一个存储入度的数组后,入度的数量有了,但实际上每门课的依赖都是具体的,所以我们肯定需要一个具体的List集合来存储每个节点,也就是每门课程的具体依赖情况。
    • 接下来的工作就好办了,先扫描一遍原依赖关系,维护好ListcourseLib,然后把入度为0的节点存入队列,进行广度优先搜索(在这其实也就是拓扑排序的思想),在搜索的过程中,不断的把入度为0的课程加入结果数组中,并更新相应的courseLib
  • 第三步,怎样知道是否能够选完全部课程?

    • 这个就比较简单了,只要最后结果数组的不小不等于课程数量,就说明不能选完全部课程。
  • 到这里,分析基本结束了,接下来就是具体的算法思路了:

    • (1)维护lists存储每个节点可到达的节点集合。
      List[] lists=new ArrayList[numCourses];
    • (2)维护courseLib存储每个节点的入度。
      int[] courseLib=new int[numCourses];
    • (3)扫描输入的图,更新listscourseLib
    • (4)获取入度为0的节点,加入队列
    • (5)BFS:将入度为0的直接加入结果数组中,并更新,对节点的入度减1,将更新后入度为0的节点继续加入队列。
    • (5)如果结果数组大小等于课程数,就返回结果数组,否则返回空数组。

0x03.解决代码–(拓扑排序?:BFS)

class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        //存储每个节点可到达的节点集合
        List<Integer>[] lists=new ArrayList[numCourses];
        //存储每个节点的入度
        int[] courseLib=new int[numCourses];
        //最终答案
        int[] result=new int[numCourses];
        //控制结果数组的下标
        int index=0;
        //扫描输入的图
        for(int[] p:prerequisites){
            courseLib[p[0]]++;
            if(lists[p[1]]==null){
                lists[p[1]]=new ArrayList<>();
            }
            lists[p[1]].add(p[0]);
        }
        Queue<Integer> queue=new LinkedList<>();
        //获取入度为0的节点,加入队列
        for(int i=0;i<numCourses;i++){
            if(courseLib[i]==0){
                queue.add(i);
            }
        }
        while(!queue.isEmpty()){
            int size=queue.size();
            while(size-->0){
                int curr=queue.poll();
                //入度为0的直接加入结果数组中
                result[index++]=curr;
                List<Integer> list=lists[curr];
                if(list==null){
                    continue;
                }
                //更新,对节点的入度减1
                for(int val:list){
                    courseLib[val]--;
                    if(courseLib[val]==0){
                        queue.add(val);
                    }
                }
            }
        }
        return index==numCourses?result:new int[0];
    }
}

ATFWUS --Writing By 2020–05-17

你可能感兴趣的:(算法)