【LeetCode题目拓展】第207题 课程表 拓展(拓扑排序、Tarjan算法、Kosaraju算法)

本文主要是介绍【LeetCode题目拓展】第207题 课程表 拓展(拓扑排序、Tarjan算法、Kosaraju算法),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 一、拓扑排序题目
    • 二、题目拓展
      • 1. 思路分析
      • 2. tarjan算法
      • 3. kosaraju算法

一、拓扑排序题目

最近在看一个算法课程的时候看到了一个比较好玩的题目的扩展,它的原题如下:
在这里插入图片描述
对应的LeetCode题目为 207. 课程表

这个题目本身来说比较简单,就是一道标准的拓扑排序题目,解法代码如下:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;public class Solution {public boolean scheduleCourses(int[][] prerequisites) {// 记录每个节点的入度int[] degree = new int[prerequisites.length]; // 记录每个节点是哪些节点的前置节点List<Integer>[] neighbors = new List[prerequisites.length]; // 记录当前可以选择的节点Queue<Integer> available = new LinkedList<>();for (int i = 0; i < prerequisites.length; i++) {degree[i] = prerequisites[i].length;neighbors[i] = new ArrayList<>();if (degree[i] == 0) {available.offer(i);}}for (int i = 0; i < prerequisites.length; i++) {for (int to : prerequisites[i]) {neighbors[to].add(i);}}int count = 0;while (!available.isEmpty()) {// 1. 取出available中一个节点// 2. 遍历该节点的邻居节点//   a. 将该邻居节点的入度减一//   b. 若此时邻居节点的入度为0,加入available中// 3. 处理节点数count加一int cur = available.poll();for (int to: neighbors[cur]) {degree[to]--;if (degree[to] == 0) {available.offer(to);}}count++;}return count == prerequisites.length;}
}

二、题目拓展

这道题目整体难度不大,但是课程里提出了对于该题目的拓展非常有意思。
题目拓展:假如学生有同时上多门课的能力,那么是否可以根据他最多能同时上课的数量,来判断对于指定的课程安排他是否可以完成。

1. 思路分析

初看这个拓展时,我的想法是在有向图里找环的方式来实现,比如找到整个有向图中包含节点数目最多的环,判断这个数目是否超过了该同学最多能同时上课的数量。但这种方式有一个比较大的问题,就是如何定义什么是环,以及该定义下的环是否满足需要。根据这两个问题,我举出了如下两个图:
在这里插入图片描述
在这里插入图片描述
可以看到,对于第一个图来说,它可以说包含3个环,这种情况下该以哪个环的节点数来度量呢?对于第二个图,两个环共用了一个节点,此时只计算一个环的节点数并不能满足题目的需求。

此时仔细观察图二中的这种场景,我发现只有这两个环都计算节点数然后与可以最大同时上课数比较才成立。结合图一,可以得出一个结论,即当图中一个节点与另一个节点可以互相到达时,它们需要被计算在一起。这不正好就是有向图的强连通分量的定义吗?

于是,这个问题就转换成了,求一个有向图中包含节点数最大的连通分量的节点数。整个思路豁然开朗了。(由此可以看出,拿到一些特化的问题时把问题迁移到一种常识性问题是非常重要的!)

根据这种思路,我们需要求有向图中规模最大的连通分量的节点数,并且把它和学生最大同时上课数进行比较,就可以得到答案了。

详细图示如下:
在这里插入图片描述
将每一个强连通分量视为学生需要同时上的课程,即可以得到一个强连通分量缩点的拓扑排序,之后学生可以按照正常的拓扑排序顺序对缩点进行上课即可,如下所示:
在这里插入图片描述

求解有向图中的强连通分量问题一般有两种算法,tarjan算法和kosaraju算法,此处不赘述两种算法的细节,感兴趣的可以自行搜索,此处只把各自解法列在下方。

2. tarjan算法

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;public class TarjanSolution {// 图的邻接表表示形式,记录每个节点是哪些节点的前置节点private List<Integer>[] neighbors;private int skill;private int[] dfn;private int[] low;private int idx;private Stack<Integer> stack;private boolean[] isInStack; // 用于快速判断是否在栈中public boolean scheduleCourses(int[][] prerequisites, int skill) {if (skill < 1) {return false;}this.skill = skill;// 初始化相关存储结构initGraph(prerequisites);// 最大连通分量的节点数return tarjan();}private void initGraph(int[][] prerequisites) {neighbors = new List[prerequisites.length]; for (int i = 0; i < prerequisites.length; i++) {neighbors[i] = new LinkedList<>();}for (int i = 0; i < prerequisites.length; i++) {for (int to : prerequisites[i]) {neighbors[to].add(i);}}}private boolean tarjan() {this.dfn = new int[neighbors.length];this.low = new int[neighbors.length];this.idx = 0;this.isInStack = new boolean[neighbors.length];this.stack = new Stack<Integer>();for (int i = 0; i < neighbors.length; ++i) {if (dfn[i] == 0) {if (!tarjanRecursion(i)) { // 如果已经失败,则提前结束return false;}}}return true;}private boolean tarjanRecursion(int cur) {// 入栈stack.push(cur);isInStack[cur] = true;//初始化当前节点的时间戳dfn[cur] = low[cur] = ++idx;// 遍历当前节点的邻居节点,共3类:1. 没被找过的;2. 在栈里的;3. 已经构成联通分量的(这种直接跳过即可)for (int neighbor: neighbors[cur]) {// 如果没被找过if (dfn[neighbor] == 0) {if (!tarjanRecursion(neighbor)) { // 如果已经失败,则提前结束return false;}low[cur] = Math.min(low[cur], low[neighbor]);} else if (isInStack[neighbor]) { // 在栈里low[cur] = Math.min(low[cur], dfn[neighbor]);}}int connectedComponentNodeNum = 0;// 若dfn==low,则当前已找到一个强连通分量,该分量节点为当前节点到栈顶的所有节点if (dfn[cur] == low[cur]) {while (cur != stack.peek()) { // 将所有非当前节点退栈int tmp = stack.pop();isInStack[tmp] = false;if (++connectedComponentNodeNum > skill) {return false;}}// 把当前节点退栈stack.pop();isInStack[cur] = false;if (++connectedComponentNodeNum > skill) {return false;}}return true;}
}

3. kosaraju算法

import java.util.LinkedList;
import java.util.List;
import java.util.Stack;public class KosarajuSolution {// 图的邻接表表示形式,记录每个节点是哪些节点的前置节点private List<Integer>[] neighbors;// 图的逆邻接表表示形式,记录每个节点是哪些节点的后置节点private List<Integer>[] rneighbors;private int skill;private boolean[] visited;private Stack<Integer> stack;public boolean scheduleCourses(int[][] prerequisites, int skill) {if (skill < 1) {return false;}this.skill = skill;// 初始化相关存储结构initGraph(prerequisites);// 最大连通分量的节点数return kosaraju();}private void initGraph(int[][] prerequisites) {neighbors = new List[prerequisites.length]; rneighbors = new List[prerequisites.length]; for (int i = 0; i < prerequisites.length; i++) {neighbors[i] = new LinkedList<>();rneighbors[i] = new LinkedList<>();}for (int i = 0; i < prerequisites.length; i++) {for (int to : prerequisites[i]) {neighbors[to].add(i);rneighbors[i].add(to);}}}private boolean kosaraju() {this.visited = new boolean[neighbors.length];this.stack = new Stack<Integer>();for (int i = 0; i < neighbors.length; ++i) { // 遍历正向图,记录出栈顺序if (!this.visited[i]) {kosarajuDfs1(i);}}while (!stack.isEmpty()) { // 从出栈最晚的节点开始,dfs遍历反向图int cur = stack.pop();if (this.visited[cur]) {if (kosarajuDfs2(cur) > skill) // 提前结束return false;}}return true;}private void kosarajuDfs1(int cur) {this.visited[cur] = true;for (int next: this.neighbors[cur]) {if (!this.visited[next]) {kosarajuDfs1(next);}}stack.push(cur);}private int kosarajuDfs2(int cur) {this.visited[cur] = false;int count = 1;for (int pre: this.rneighbors[cur]) {if (this.visited[pre]) {if (count > this.skill) return count; // 提前结束count += kosarajuDfs2(pre);}}return count;}
}

这篇关于【LeetCode题目拓展】第207题 课程表 拓展(拓扑排序、Tarjan算法、Kosaraju算法)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/486856

相关文章

openCV中KNN算法的实现

《openCV中KNN算法的实现》KNN算法是一种简单且常用的分类算法,本文主要介绍了openCV中KNN算法的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录KNN算法流程使用OpenCV实现KNNOpenCV 是一个开源的跨平台计算机视觉库,它提供了各

springboot+dubbo实现时间轮算法

《springboot+dubbo实现时间轮算法》时间轮是一种高效利用线程资源进行批量化调度的算法,本文主要介绍了springboot+dubbo实现时间轮算法,文中通过示例代码介绍的非常详细,对大家... 目录前言一、参数说明二、具体实现1、HashedwheelTimer2、createWheel3、n

Mybatis 传参与排序模糊查询功能实现

《Mybatis传参与排序模糊查询功能实现》:本文主要介绍Mybatis传参与排序模糊查询功能实现,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录一、#{ }和${ }传参的区别二、排序三、like查询四、数据库连接池五、mysql 开发企业规范一、#{ }和${ }传参的

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时

C++快速排序超详细讲解

《C++快速排序超详细讲解》快速排序是一种高效的排序算法,通过分治法将数组划分为两部分,递归排序,直到整个数组有序,通过代码解析和示例,详细解释了快速排序的工作原理和实现过程,需要的朋友可以参考下... 目录一、快速排序原理二、快速排序标准代码三、代码解析四、使用while循环的快速排序1.代码代码1.由快

如何通过Golang的container/list实现LRU缓存算法

《如何通过Golang的container/list实现LRU缓存算法》文章介绍了Go语言中container/list包实现的双向链表,并探讨了如何使用链表实现LRU缓存,LRU缓存通过维护一个双向... 目录力扣:146. LRU 缓存主要结构 List 和 Element常用方法1. 初始化链表2.

golang字符串匹配算法解读

《golang字符串匹配算法解读》文章介绍了字符串匹配算法的原理,特别是Knuth-Morris-Pratt(KMP)算法,该算法通过构建模式串的前缀表来减少匹配时的不必要的字符比较,从而提高效率,在... 目录简介KMP实现代码总结简介字符串匹配算法主要用于在一个较长的文本串中查找一个较短的字符串(称为

通俗易懂的Java常见限流算法具体实现

《通俗易懂的Java常见限流算法具体实现》:本文主要介绍Java常见限流算法具体实现的相关资料,包括漏桶算法、令牌桶算法、Nginx限流和Redis+Lua限流的实现原理和具体步骤,并比较了它们的... 目录一、漏桶算法1.漏桶算法的思想和原理2.具体实现二、令牌桶算法1.令牌桶算法流程:2.具体实现2.1

Spring排序机制之接口与注解的使用方法

《Spring排序机制之接口与注解的使用方法》本文介绍了Spring中多种排序机制,包括Ordered接口、PriorityOrdered接口、@Order注解和@Priority注解,提供了详细示例... 目录一、Spring 排序的需求场景二、Spring 中的排序机制1、Ordered 接口2、Pri