感谢力扣!
提示
现在你总共有
numCourses
门课需要选,记为0
到numCourses - 1
。给你一个数组prerequisites
,其中prerequisites[i] = [ai, bi]
,表示在选修课程ai
前 必须 先选修bi
。
- 例如,想要学习课程
0
,你需要先完成课程1
,我们用一个匹配来表示:[0,1]
。返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。
示例 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,2,1,3] 解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。 因此,一个正确的课程顺序是[0,1,2,3]
。另一个正确的排序是[0,2,1,3]
。示例 3:
输入:numCourses = 1, prerequisites = [] 输出:[0]提示:
1 <= numCourses <= 2000
0 <= prerequisites.length <= numCourses * (numCourses - 1)
prerequisites[i].length == 2
0 <= ai, bi < numCourses
ai != bi
- 所有
[ai, bi]
互不相同
1.分析题目
应该是一个有向图的问题,可是我不会,我来解的话,先把依赖关系转化为List,存放所有依赖关系,把依赖关系为空的先学了,然后学完以后把和这个依赖相关的课程全部解锁,那万一一个课程依赖两个前置可能呢?而且时间复杂度是要飙升到n^n吗?过于粗糙了吧。好像可以用List把某个元素移除的吧,那我学了就移除不就可以了吗?移除了以后如果没有可以移除的就结束循环,好吧,感觉思路是可以,就是循环太多次了,写了然后学习吧:
① 按照自己的想法把思路写出来;
② 看题解
③ 根据题解再写一遍
④ 总结
2.编写代码
好的,写着写着IDEA跑都跑不起来,转了五分钟圈了,放上来凌辱一下自己,希望以后记住,不要逞能,看你 写的是些什么东西。
public int[] findOrder(int numCourses, int[][] prerequisites) {
HashMap<Integer, List<Integer>> hashMap = new HashMap<>();
List<Integer> result = new ArrayList<>();
for (int i = 0; i < prerequisites.length; i++) {
int[] pre = prerequisites[i];
List<Integer> sonList = hashMap.computeIfAbsent(pre[1], key -> new ArrayList<>());
sonList.add(pre[0]);
}
while(!hashMap.isEmpty()){
for (Integer i : hashMap.keySet()) {
List<Integer> courses = hashMap.get(i);
if(courses.isEmpty()){
result.add(i);
// 移除所有已学的课程
for (Integer j : hashMap.keySet()) {
List<Integer> remove = hashMap.get(j);
remove.remove(i);
}
hashMap.remove(i);
}
}
}
return result.stream().mapToInt(Integer::valueOf).toArray();
}
老老实实看题解吧,这就是现实。
3.学习题解
题解用的是深度优先和广度优先搜索遍历,BFS和DFS,一直都不知道用JAVA怎么写,大概是了解用递归,当初学的时候用的好像是C语言,还用的struct,原理还是记得,深度就是撞到南墙再回头,广度就是花心撒网海王。哈哈,鉴于时间有限,直接看了去理解,理解的办法就是把题解的代码拿下来,自己注释一遍,然后再根据自己的理解自己打一遍,加油!
3.1 深度搜索
感谢力扣的官方题解:
class Solution {
// 存储有向图
List<List<Integer>> edges;
// 标记每个节点的状态:0=未搜索,1=搜索中,2=已完成
int[] visited;
// 用数组来模拟栈,下标 n-1 为栈底,0 为栈顶
int[] result;
// 判断有向图中是否有环
boolean valid = true;
// 栈下标
int index;
public int[] findOrder(int numCourses, int[][] prerequisites) {
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
visited = new int[numCourses];
result = new int[numCourses];
index = numCourses - 1;
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
}
// 每次挑选一个「未搜索」的节点,开始进行深度优先搜索
for (int i = 0; i < numCourses && valid; ++i) {
if (visited[i] == 0) {
dfs(i);
}
}
if (!valid) {
return new int[0];
}
// 如果没有环,那么就有拓扑排序
return result;
}
public void dfs(int u) {
// 将节点标记为「搜索中」
visited[u] = 1;
// 搜索其相邻节点
// 只要发现有环,立刻停止搜索
for (int v: edges.get(u)) {
// 如果「未搜索」那么搜索相邻节点
if (visited[v] == 0) {
dfs(v);
if (!valid) {
return;
}
}
// 如果「搜索中」说明找到了环
else if (visited[v] == 1) {
valid = false;
return;
}
}
// 将节点标记为「已完成」
visited[u] = 2;
// 将节点入栈
result[index--] = u;
}
}
作者:力扣官方题解
链接:https://leetcode.cn/problems/course-schedule-ii/solutions/249149/ke-cheng-biao-ii-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
简单理解了一遍,巧妙啊,还能这样的,其实我不用移除元素,我只要知道他已经学过就行了,根据元素值来判断就好了,差不多明白了,这是添加自己注释的版本:
// 存储有向图
// cz:竟然要用到全局变量,一个存放整形列表的列表,表中表可还行
List<List<Integer>> edges;
// 标记每个节点的状态:0=未搜索,1=搜索中,2=已完成
// cz:定义节点状态,所以第二部是要根据题解明确有哪些状态,有可能0,1,2就是万用公式
int[] visited;
// 用数组来模拟栈,下标 n-1 为栈底,0 为栈顶
// cz:那为啥不用Stack,不过Stack和以前那个双向链的时空复杂度太吓人了,待会要看一下,写个批注过去
// cz:而且这个只适用于已经知道栈大小的情况,这里就是课程的个数吧
int[] result;
// 判断有向图中是否有环
// cz:需求是要有向无环图,因为课程不能有互为前置课程这种需求
boolean valid = true;
// 栈下标
// cz:还需要专门标识的下标,酷
int index;
public int[] findOrder(int numCourses, int[][] prerequisites) {
// cz:初始化表中表
edges = new ArrayList<List<Integer>>();
// cz:对每门课程初始化一个列表,诶,我的思路也是这样诶
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
// cz:根据课程个数初始化状态数组
visited = new int[numCourses];
// cz:根据课程个数初始化模拟栈的数组
result = new int[numCourses];
// cz:从最后一门课程开始处理?
// cz:从栈底开始存,即第一门满足条件的课程,存完以后直接按顺序输出就是学习顺序;
// cz2:从栈底开始存,第一个没有被依赖的课程一定是最后学的课程,因为被依赖得太多了
// cz2:这个题目是倒着求的!
index = numCourses - 1;
// cz:遍历前置课程数组
for (int[] info : prerequisites) {
// cz:把所有课程的前置课程关系保存下来,以明确这门课程是哪些课程的前置课程
// cz:如果有一门课程列表长度为0,那么这个课程就可以直接学。
// cz:所以怎么解决要遍历太多次的问题呢,到这里我都是一样的
// cz2:完全不一样好吧,如果有一门课程列表长度为0,不是任何课程的前置课程证明学习它要很多前置课程
// cz2:意味着需要最后学的!我想反了,没有理解清楚。
edges.get(info[1]).add(info[0]);
}
// 每次挑选一个「未搜索」的节点,开始进行深度优先搜索
// cz:来了!从编号0开始遍历所有的课程,如果有环同时跳出,为什么这里是++i,不能是i++啊?
for (int i = 0; i < numCourses && valid; ++i) {
// 如果当前课程是未搜索的状态则进行dfs
if (visited[i] == 0) {
// 深度优先搜索算法 真的是递归!?
dfs(i);
}
}
if (!valid) {
return new int[0];
}
// 如果没有环,那么就有拓扑排序
return result;
}
public void dfs(int u) {
// 将节点标记为「搜索中」
// cz:只要进来一次就是搜索中
visited[u] = 1;
// 搜索其相邻节点
// 只要发现有环,立刻停止搜索
// cz:把依赖当前课程的所有课程遍历一遍
for (int v: edges.get(u)) {
// 如果「未搜索」那么搜索相邻节点
// cz:判断依赖当前课程的课程是不是被搜索过
if (visited[v] == 0) {
// cz:如果没有被搜索过继续递归调用
dfs(v);
// cz:如果已经找到了环valid = false
if (!valid) {
// 返回,避免浪费资源继续后面的递归
return;
}
}
// 如果「搜索中」说明找到了环
// cz:如果正在搜索过程中,证明该课程不仅依赖当前课程,还被依赖环里面的其他课程依赖
// cz:如果是等于2的话,相当于这个课程已经学过了可以跳过,巧妙啊
// cz2:被半小时前的自己蠢cry了啊,如果等于2的话,这个课程已经没有依赖关系了
// cz2:也就是已经把这门课给丢到老后面学了,看别的就行了
else if (visited[v] == 1) {
// 证明找到了环,把标志变量改为false
valid = false;
// 直接返回,不用浪费资源了
return;
}
}
// 将节点标记为「已完成」
// cz:如果能走到这一步有两个情况:
// cz:情况1:这门课不是任何一门课程的前置课程,即edges.get(u)的长度为0
// cz:情况2:依赖这门课程的所有课程都已经为2,即学习过;
// cz2:情况2:依赖这门课程的所有课程都没有了依赖关系,可以安排到后面学习了;
visited[u] = 2;
// 将节点入栈
// cz:根据先后顺序入栈
result[index--] = u;
}
我拿一个例题+EXCEL人脑跑一遍,加深一下理解
输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]] | ||||||||
过程: | ||||||||
步骤 | index | valid | edges | visited | result | v | i | 备注 |
初始化 | 3 | TRUE | [[],[],[],[]] | [0,0,0,0] | [0,0,0,0] | 0 | / | 根据输入初始化 |
转化原始数据 | 3 | TRUE | [[1,2],[3],[3],[]] | [0,0,0,0] | [0,0,0,0] | 0 | / | 转换依赖数组形式 |
检查课程0 | 3 | TRUE | [[1,2],[3],[3],[]] | [0,0,0,0] | [0,0,0,0] | 0 | 0 | 0未被搜索 |
1次DFS | 3 | TRUE | [[1,2],[3],[3],[]] | [1,0,0,0] | [0,0,0,0] | 0 | 0 | 0被搜索了,发现1没有被搜索过 |
1.1次DFS | 3 | TRUE | [[1,2],[3],[3],[]] | [1,1,0,0] | [0,0,0,0] | 1 | 0 | 1被搜索了,发现3没有被搜索过 |
1.1.1次DFS | 3 | TRUE | [[1,2],[3],[3],[]] | [1,1,0,1] | [0,0,0,0] | 3 | 0 | 3被搜索了,发现3不被任何课程依赖 |
1.1.1得解 | 2 | TRUE | [[1,2],[3],[3],[]] | [1,1,0,2] | [0,0,0,3] | 3 | 0 | 3作为解放在栈底 |
1.1次DFS | 2 | TRUE | [[1,2],[3],[3],[]] | [1,1,0,2] | [0,0,0,3] | 1 | 0 | 收到1.1.1的返回后,发现所有课程已经没有依赖关系 |
1.1得解 | 1 | TRUE | [[1,2],[3],[3],[]] | [1,2,0,2] | [0,0,1,3] | 1 | 0 | 1做为解压进了结果栈 |
1次DFS | 1 | TRUE | [[1,2],[3],[3],[]] | [1,2,0,2] | [0,0,1,3] | 0 | 0 | 收到1.1的返回后,发现2没有被搜索过 |
1.2次DFS | 1 | TRUE | [[1,2],[3],[3],[]] | [1,2,1,2] | [0,0,1,3] | 2 | 0 | 2被搜索了,发现所有课程已经没有依赖关系 |
1.2得解 | 0 | TRUE | [[1,2],[3],[3],[]] | [1,2,2,2] | [0,2,1,3] | 2 | 0 | 2作为节压进了结果栈 |
1次DFS | 0 | TRUE | [[1,2],[3],[3],[]] | [1,2,2,2] | [0,2,1,3] | 0 | 0 | 收到1.2的返回后,发现所有课程已经没有依赖关系 |
1得解 | -1 | TRUE | [[1,2],[3],[3],[]] | [2,2,2,2] | [0,2,1,3] | 0 | 0 | 0作为解压进了结果栈 |
检查课程1 | -1 | TRUE | [[1,2],[3],[3],[]] | [2,2,2,2] | [0,2,1,3] | 0 | 1 | 1已完成搜索 |
检查课程2 | -1 | TRUE | [[1,2],[3],[3],[]] | [2,2,2,2] | [0,2,1,3] | 0 | 2 | 2已完成搜索 |
检查课程3 | -1 | TRUE | [[1,2],[3],[3],[]] | [2,2,2,2] | [0,2,1,3] | 0 | 3 | 3已完成搜索 |
算法得解 | -1 | TRUE | [[1,2],[3],[3],[]] | [2,2,2,2] | [0,2,1,3] | 0 | 4 | 已经完成题解 |
结果:[0,2,1,3] |
不知道形式怎么样,好像表格有点大,待会发布了看一下,不行就转成图片出来,理解算法的过程中发现自己想的还是太天真了,其实和自己的理解不一样,为了避免水字数的嫌疑,我第一次标记的注释用cz:,第二次标注的注释用cz2:来区分一下。最后自己写一下代码,看能不能写出来;
自己写的:
List<List<Integer>> tempList;
int[] status;
int[] result;
int index;
boolean flag = false;
public int[] findOrder(int numCourses, int[][] prerequisites) {
tempList = new ArrayList<List<Integer>>();
status = new int[numCourses];
result = new int[numCourses];
index = numCourses- 1;
for (int i = 0; i < numCourses; i++) {
tempList.add(new ArrayList<Integer>());
}
// 改变数据结构
for (int[] pre : prerequisites) {
tempList.get(pre[1]).add(pre[0]);
}
// 遍历求解
for(int i=0;i<numCourses&&!flag;i++){
if(status[i]==0){
dfs(i);
}
if(flag){
return new int[0];
}
}
return result;
}
void dfs(int v){
status[v] = 1;
// 检查是否有下一级依赖
for(Integer depend:tempList.get(v)){
if (status[depend] == 0) {
dfs(depend);
}
if(status[depend]== 1){
flag = true;
return ;
}
}
status[v] = 2;
result[index--] = v;
}
感动,四天终于过了:
3.2 广度搜索
这个题目其实还没有想过怎么用广度搜索做,原来我之前错的解法就是广度搜索啊,潸然泪下,那根据这个题目我感觉我可以做出来了,稍微缓一下脑子,按照同样的思路写一下广搜的代码,尽量不看题解的具体代码,加油。
写了也过了,但是我改了数据结构,简单改了几行代码就可以了,然后看了题解发现真正的广搜不应该改数据结构,我改的地方我列一下:
// index = numCourses- 1;
index = 0;
// tempList.get(pre[1]).add(pre[0]);
tempList.get(pre[0]).add(pre[1]);
// dfs(i);
efs(i);
// result[index++] = v;
result[index--] = v;
我以为的只是我以为的,代码用了队列,也是对JAVA的一个熟悉,那就按照深度搜索的套路继续来吧,先对着把代码注释一遍,然后用excel自己跑一下,自己写一下:
感谢力扣官方题解:
class Solution {
// 存储有向图
List<List<Integer>> edges;
// 存储每个节点的入度
int[] indeg;
// 存储答案
int[] result;
// 答案下标
int index;
public int[] findOrder(int numCourses, int[][] prerequisites) {
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
indeg = new int[numCourses];
result = new int[numCourses];
index = 0;
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
++indeg[info[0]];
}
Queue<Integer> queue = new LinkedList<Integer>();
// 将所有入度为 0 的节点放入队列中
for (int i = 0; i < numCourses; ++i) {
if (indeg[i] == 0) {
queue.offer(i);
}
}
while (!queue.isEmpty()) {
// 从队首取出一个节点
int u = queue.poll();
// 放入答案中
result[index++] = u;
for (int v: edges.get(u)) {
--indeg[v];
// 如果相邻节点 v 的入度为 0,就可以选 v 对应的课程了
if (indeg[v] == 0) {
queue.offer(v);
}
}
}
if (index != numCourses) {
return new int[0];
}
return result;
}
}
作者:力扣官方题解
链接:https://leetcode.cn/problems/course-schedule-ii/solutions/249149/ke-cheng-biao-ii-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
注释后结果如下:
// 存储有向图
// cz:我理解是重新建立一个更好处理的、有规律的数据结构
List<List<Integer>> edges;
// 存储每个节点的入度
// cz:看每门课程依赖了多少门课程
int[] indeg;
// 存储答案
// cz:存储最后应该给出去的答案
int[] result;
// 答案下标
// cz:答案控制下标
int index;
public int[] findOrder(int numCourses, int[][] prerequisites) {
// cz:创建数据结构
edges = new ArrayList<List<Integer>>();
// cz:遍历课程个数 初始化有向图
for (int i = 0; i < numCourses; ++i) {
// cz:为每一个新建一个列表
edges.add(new ArrayList<Integer>());
}
// cz:初始化依赖多少门课程的数组
indeg = new int[numCourses];
// cz:初始化学习顺序数组
result = new int[numCourses];
// cz:从要学的第一门开始
index = 0;
// cz:遍历前置课程数组
for (int[] info : prerequisites) {
// cz:把每门课程被依赖的数据记录下来
edges.get(info[1]).add(info[0]);
// cz:同时记录这门课程依赖了几门课程
++indeg[info[0]];
}
// cz:生命一个整形队列
Queue<Integer> queue = new LinkedList<Integer>();
// 将所有入度为 0 的节点放入队列中
// cz:每门课程遍历看一下
for (int i = 0; i < numCourses; ++i) {
// cz:没有依赖其他课程
if (indeg[i] == 0) {
// cz:入队记录下来准备直接学
queue.offer(i);
}
}
// cz:把队列遍历一遍,即所有学过的去掉以后还有没有没有依赖的课程
while (!queue.isEmpty()) {
// 从队首取出一个节点
// cz:拿出第一个没有依赖的课程,出队一个准备直接学的课程
int u = queue.poll();
// 放入答案中
// cz:没有依赖的课程可以直接学
result[index++] = u;
// cz:学了的课程遍历找一下依赖这门课程的其他课程看一下
for (int v: edges.get(u)) {
// cz:因为已经学了一门课程,所以找到的课程都少了一门课程依赖
--indeg[v];
// 如果相邻节点 v 的入度为 0,就可以选 v 对应的课程了
// cz:如果这门课程少了一门以后就没有依赖别的课程了
if (indeg[v] == 0) {
// cz:入队记录下来准备直接学
queue.offer(v);
}
}
}
// cz:直接学的课程没有等于总的课程数量
if (index != numCourses) {
// cz:无法建立学习顺序
return new int[0];
}
// cz:有学习顺序,返回结果
return result;
}
用Excel跑一遍:
输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]] | ||||||
过程: | ||||||
step | index | indeg | edges | queue | result | remark |
初始化 | 0 | [0,0,0,0] | [[],[],[],[]] | / | [0,0,0,0] | 根据输入初始化 |
转换原始数据 | 0 | [0,1,1,2] | [[1,2],[3],[3],[]] | / | [0,0,0,0] | 转换依赖数组形式 |
队列初始化 | 0 | [0,1,1,2] | [[1,2],[3],[3],[]] | [0] | [0,0,0,0] | 把没有依赖的课程入队 |
第一次出队 | 1 | [0,1,1,2] | [[1,2],[3],[3],[]] | / | [0,0,0,0] | 学0 |
减少依赖 | 1 | [0,0,0,2] | [[1,2],[3],[3],[]] | [2]→[1] | [0,0,0,0] | 1、2可以学了入队 |
第二次出队 | 2 | [0,0,0,2] | [[1,2],[3],[3],[]] | [2] | [0,1,0,0] | 学1(先进先出) |
减少依赖 | 2 | [0,0,0,1] | [[1,2],[3],[3],[]] | [2] | [0,1,0,0] | 3减少了依赖还不能学 |
第三次出队 | 3 | [0,0,0,1] | [[1,2],[3],[3],[]] | / | [0,1,2,0] | 学2 |
减少依赖 | 3 | [0,0,0,0] | [[1,2],[3],[3],[]] | [3] | [0,1,2,0] | 3可以学了入队 |
第四次出队 | 4 | [0,0,0,0] | [[1,2],[3],[3],[]] | / | [0,1,2,3] | 学3 |
减少依赖 | 4 | [0,0,0,0] | [[1,2],[3],[3],[]] | / | [0,1,2,3] | 3不被课程依赖,不动 |
算法得解 | 4 | [0,0,0,0] | [[1,2],[3],[3],[]] | / | [0,1,2,3] | 已经完成题解 |
结果:[0,2,1,3] |
自己再写一遍,记得队列的初始化是:Queue<Object> var = new LinkedList<Object>();好!加油,开始写。
List<List<Integer>> temp = new ArrayList<List<Integer>>();
int index;
int[] result;
int[] count;
public int[] findOrder(int numCourses, int[][] prerequisites) {
index = 0;
result = new int[numCourses];
count = new int[numCourses];
for (int i = 0; i < numCourses; i++) {
temp.add(new ArrayList<Integer>());
}
// 转换为有向图并统计依赖课程数量
for (int[] pre : prerequisites) {
temp.get(pre[1]).add(pre[0]);
count[pre[0]]++;
}
// 建立队列
Queue<Integer> queue = new LinkedList<Integer>();
// 没有依赖的入队
for (int i = 0; i < numCourses; i++) {
if (count[i] == 0) {
queue.offer(i);
}
}
// 遍历队列
while(!queue.isEmpty()){
int current = queue.poll();
result[index++] = current;
for (Integer depend : temp.get(current)) {
count[depend]-=1;
if(count[depend]==0){
queue.offer(depend);
}
}
}
// 判断是否求解完成
if (index != numCourses) {
return new int[0];
}
return result;
}
不知道为什么这么慢,具体的思路明白了,贴一下结果。
4.反思总结
这篇文章写了我三天,总用时应该是早上1小时(07:00~08:00)+下午4个小时(15:30~19:30),搞笑,这种学过的还要弄5个小时。希望能够正视自己的能力,对于好的多鼓励,对于不好的不要遮掩,不要以为做什么都能做好,无论做什么都要保持一颗谦卑,踏实的心态,学如逆水行舟,不进则退。看了那么多思维方法,不会应用,学了那么多时间管理,没有踏实去做,就什么都不是,希望自己加油!
ps:忘记了,这次做的是拓扑排序来着,用深度搜索的方法应该任何一个图都能做到拓扑排序吧?
① 学习了队列的声明Queue<Object> var = new LinkedList<Object>();,入队var.offer(),出队var.poll();
② 思路+题解+Excel+打的方式可以作为经验后续继续执行,比漫无目的的瞎想要好。