一刷307-剑指 Offer II 113. 课程顺序(210. 课程表 II)

题目:
现在总共有 numCourses 门课需要选,记为 0 到 numCourses-1。

给定一个数组 prerequisites ,它的每一个元素 prerequisites[i] 表示两门课程之间的先修顺序。
 例如 prerequisites[i] = [ai, bi] 表示想要学习课程 ai ,需要先完成课程 bi 。

请根据给出的总课程数  numCourses 和表示先修顺序的 prerequisites 得出一个可行的修课序列。
可能会有多个正确的顺序,只要任意返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
--------------------
示例 1:
输入: numCourses = 2, prerequisites = [[1,0]] 
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:

输入: numCourses = 4, prerequisites = [[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] 。
示例 3:

输入: numCourses = 1, prerequisites = [] 
输出: [0]
解释: 总共 1 门课,直接修第一门课就可。
 
提示:
1 <= numCourses <= 2000
0 <= prerequisites.length <= numCourses * (numCourses - 1)
prerequisites[i].length == 2
0 <= ai, bi < numCourses
ai != bi
prerequisites 中不存在重复元素
-----------------
思路:
先说最重要的部分:「拓扑排序」是专门应用于有向图的算法;
这道题用 BFS 和 DFS 都可以完成,只需要掌握 BFS 的写法就可以了,BFS 的写法很经典;
BFS 的写法就叫「拓扑排序」,这里还用到了贪心算法的思想,
贪的点是:当前让入度为 0 的那些结点入队;
「拓扑排序」的结果不唯一;
删除结点的操作,通过「入度数组」体现,这个技巧要掌握;
「拓扑排序」的一个附加效果是:能够顺带检测有向图中是否存在环,这个知识点非常重要,
如果在面试的过程中遇到这个问题,要把这一点说出来。
具有类似附加功能的算法还有:Bellman-Ford 算法附加的作用是可以用于检测是否有负权环
--------------------

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

如果优先图中,存在环,拓扑排序不能继续得到入度值为 0 的节点,退出循环,
此时图中存在没有遍历到的节点,说明图中存在环。

此时说明课程设计不合理,有循环依赖。

在这里插入图片描述

拓扑排序实际上应用的是贪心算法,贪心算法简而言之:每一步最优,则全局最优。

具体到拓扑排序,每一次都从图中删除没有前驱的顶点,这里并不需要真正的做删除操作,
我们可以设置一个入度数组,每一轮都输出入度为 0 的结点,并移除它、修改它指向的结点的入度(-1即可),
依次得到的结点序列就是拓扑排序的结点序列。如果图中还有结点没有被移除,
则说明“不能完成所有课程的学习”。

拓扑排序保证了每个活动(在这题中是“课程”)的所有前驱活动都排在该活动的前面,
并且可以完成所有活动。拓扑排序的结果不唯一。拓扑排序还可以用于检测一个有向图是否有环。
相关的概念还有 AOV 网,这里就不展开了。

算法流程:
1、在开始排序前,扫描对应的存储空间(使用邻接表),将入度为 0 的结点放入队列。
2、只要队列非空,就从队首取出入度为 0 的结点,将这个结点输出到结果集中,
并且将这个结点的所有邻接结点(它指向的结点)的入度减 1,在减 1 以后,
如果这个被减 1 的结点的入度为 0 ,就继续入队。

3、当队列为空的时候,检查结果集中的顶点个数是否和课程数相等即可。
(思考这里为什么要使用队列?如果不用队列,还可以怎么做,会比用队列的效果差还是更好?)
在代码具体实现的时候,除了保存入度为 0 的队列,我们还需要两个辅助的数据结构:

1、邻接表:通过结点的索引,我们能够得到这个结点的后继结点;
2、入度数组:通过结点的索引,我们能够得到指向这个结点的结点个数。
这个两个数据结构在遍历题目给出的邻边以后就可以很方便地得到。
----------------
```java
复杂度分析:
时间复杂度:O(E + V)。这里 E 表示邻边的条数,V 表示结点的个数。
初始化入度为 0 的集合需要遍历整张图,具体做法是检查每个结点和每条边,
因此复杂度为 O(E+V),然后对该集合进行操作,又需要遍历整张图中的每个结点和每条边,
复杂度也为 O(E+V)
空间复杂度:O(V):入度数组、邻接表的长度都是结点的个数 V,
即使使用队列,队列最长的时候也不会超过 V,因此空间复杂度是 O(V)
--------------------
BFS 的总体思路:

建立入度表,入度为 0 的节点先入队
当队列不为空,节点出队,标记学完课程数量的变量加 1,并记录该课程
将课程的邻居入度减 1
若邻居课程入度为 0,加入队列
用一个变量记录学完的课程数量,一个数组记录最终结果,简洁好理解。
-------------------
class Solution { // 方法 1 最简单的 BFS
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        if (numCourses == 0) return new int[0];
        int[] inDegrees = new int[numCourses];
        for (int[] p : prerequisites) {  // 建立入度表 对于有先修课的课程,计算有几门先修课
            inDegrees[p[0]]++;
        }
        Queue<Integer> queue = new LinkedList<>();// 入度为0的节点队列
        for (int i = 0; i < inDegrees.length; i++) {
            if (inDegrees[i] == 0) queue.offer(i);
        }
        int count = 0;  // 记录可以学完的课程数量
        int[] res = new int[numCourses];  // 可以学完的课程
        while (!queue.isEmpty()){ // 根据提供的先修课列表,删除入度为 0 的节点
            int curr = queue.poll();
            res[count++] = curr;   // 将可以学完的课程加入结果当中
            for (int[] p : prerequisites) {
                if (p[1] == curr){
                    inDegrees[p[0]]--;
                    if (inDegrees[p[0]] == 0) queue.offer(p[0]);
                }
            }
        }
        if (count == numCourses) return res;
        return new int[0];
    }
}

LC

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值