【面试】运算器-⑨课程表

210. 课程表 II

感谢力扣!

提示

现在你总共有 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]]
过程:
步骤indexvalidedgesvisitedresultvi备注
初始化3TRUE[[],[],[],[]][0,0,0,0][0,0,0,0]0/根据输入初始化
转化原始数据3TRUE[[1,2],[3],[3],[]][0,0,0,0][0,0,0,0]0/转换依赖数组形式
检查课程03TRUE[[1,2],[3],[3],[]][0,0,0,0][0,0,0,0]000未被搜索
1次DFS3TRUE[[1,2],[3],[3],[]][1,0,0,0][0,0,0,0]000被搜索了,发现1没有被搜索过
1.1次DFS3TRUE[[1,2],[3],[3],[]][1,1,0,0][0,0,0,0]101被搜索了,发现3没有被搜索过
1.1.1次DFS3TRUE[[1,2],[3],[3],[]][1,1,0,1][0,0,0,0]303被搜索了,发现3不被任何课程依赖
1.1.1得解2TRUE[[1,2],[3],[3],[]][1,1,0,2][0,0,0,3]303作为解放在栈底
1.1次DFS2TRUE[[1,2],[3],[3],[]][1,1,0,2][0,0,0,3]10收到1.1.1的返回后,发现所有课程已经没有依赖关系
1.1得解1TRUE[[1,2],[3],[3],[]][1,2,0,2][0,0,1,3]101做为解压进了结果栈
1次DFS1TRUE[[1,2],[3],[3],[]][1,2,0,2][0,0,1,3]00收到1.1的返回后,发现2没有被搜索过
1.2次DFS1TRUE[[1,2],[3],[3],[]][1,2,1,2][0,0,1,3]202被搜索了,发现所有课程已经没有依赖关系
1.2得解0TRUE[[1,2],[3],[3],[]][1,2,2,2][0,2,1,3]202作为节压进了结果栈
1次DFS0TRUE[[1,2],[3],[3],[]][1,2,2,2][0,2,1,3]00收到1.2的返回后,发现所有课程已经没有依赖关系
1得解-1TRUE[[1,2],[3],[3],[]][2,2,2,2][0,2,1,3]000作为解压进了结果栈
检查课程1-1TRUE[[1,2],[3],[3],[]][2,2,2,2][0,2,1,3]011已完成搜索
检查课程2-1TRUE[[1,2],[3],[3],[]][2,2,2,2][0,2,1,3]022已完成搜索
检查课程3-1TRUE[[1,2],[3],[3],[]][2,2,2,2][0,2,1,3]033已完成搜索
算法得解-1TRUE[[1,2],[3],[3],[]][2,2,2,2][0,2,1,3]04已经完成题解
结果:[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]]
过程:
stepindexindegedgesqueueresultremark
初始化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+打的方式可以作为经验后续继续执行,比漫无目的的瞎想要好。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值