Leecode 207. 课程表

题目

你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]
给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?

来源:力扣(LeetCode)
链接

/**
 * @ClassName CanFinish
 * @Description 你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。
 *
 * 在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]
 *
 * 给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?
 *
 * 示例 1:
 *
 * 输入: 2, [[1,0]]
 * 输出: true
 * 解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。
 * 示例 2:
 *
 * 输入: 2, [[1,0],[0,1]]
 * 输出: false
 * 解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。
 *  
 *
 * 提示:
 *
 * 输入的先决条件是由 边缘列表 表示的图形,而不是 邻接矩阵 。详情请参见图的表示法。
 * 你可以假定输入的先决条件中没有重复的边。
 * 1 <= numCourses <= 10^5
 *
 * @Author Kingsley
 * @Date 2020/8/4 10:45
 * @Version 1.0
 **/
public class CanFinish {

    /**
     * 算法流程:
     * 统计课程安排图中每个节点的入度,生成 入度表 indegrees。
     * 借助一个队列 queue,将所有入度为 0 的节点入队。
     * 当 queue 非空时,依次将队首节点出队,在课程安排图中删除此节点 pre:
     * 并不是真正从邻接表中删除此节点 pre,而是将此节点对应所有邻接节点 cur 的入度 -1−1,即 indegrees[cur] -= 1。
     * 当入度 -1后邻接节点 cur 的入度为 0,说明 cur 所有的前驱节点已经被 “删除”,此时将 cur 入队。
     * 在每次 pre 出队时,执行 numCourses--;
     * 若整个课程安排图是有向无环图(即可以安排),则所有节点一定都入队并出队过,即完成拓扑排序。换个角度说,
     * 若课程安排图中存在环,一定有节点的入度始终不为 00。
     * 因此,拓扑排序出队次数等于课程个数,返回 numCourses == 0 判断课程是否可以成功安排。
     *
     * 复杂度分析:
     * 时间复杂度 O(N + M)O(N+M): 遍历一个图需要访问所有节点和所有临边,NN 和 MM 分别为节点数量和临边数量;
     * 空间复杂度 O(N + M)O(N+M): 为建立邻接表所需额外空间,adjacency 长度为 NN ,并存储 MM 条临边的数据
     * @param numCourses
     * @param prerequisites
     * @return
     */
    //BFS
    public static boolean canFinish(int numCourses, int[][] prerequisites) {
        //入度表
        int[] indegrees = new int[numCourses];
        //邻接表
        List<List<Integer>>  adjacency = new ArrayList<>();
        //队列
        Queue<Integer> queue = new LinkedList<>();
        //先往邻接表中加入代表课程数量的固定个数的链表。比如有10个课程,就会加入十个链表。因为每个链表都有自己的邻接表
        for(int i = 0; i < numCourses; i++){
            adjacency.add(new ArrayList<>());
        }

        //初始化邻接表
        for(int[] cp : prerequisites){
            //入度表值++
            indegrees[cp[0]]++;
            //init the adjacency, 学习cp[0]之前必须完成cp[1]的学习
            //就是往链表中放入当前节点的所有有链接的后续节点
            /**
             * *    c1    c2
             *    /  \  /  \
             *   c8   c3    c5
             *   数组中的表示就是{{c3,c1}, {c8, c1}, {c3,c2}, {c5,c2}}
             *   这里就会往属于c1的链表中加入 c8 和 c3
             */
            adjacency.get(cp[1]).add(cp[0]);
        }
        //如果入度为0,代表该课程可以直接学习,不需要以学习了别的课程为前提
        //加入队列,成为第一批出队课程
        for(int i = 0; i < numCourses; i++){
            if(indegrees[i] == 0)
                queue.offer(i);
        }

        //BFS
        while (!queue.isEmpty()) {
            int pre = queue.poll();
            //出队,代表有一门课程已经可以学习完成
            numCourses--;
            //该课程出队列,那么以该课程为前导课程的那些节点的课程的入度就可以-1;
            /**
             *    c1    c2
             *   /  \  /  \
             *  c8   c3    c5
             * c1没有前导课程,c3,c8以c1作为前导课程。 那么当c1出队的时候,c8和c3的入度就应该-1;
             * 此时c8的入度为0,可以入队了。c3的入度变为1,暂时不能入队。
             * 数组中的表示就是{{c3,c1}, {c8, c1}, {c3,c2}, {c5,c2}}
             */
            for(int cur : adjacency.get(pre))
                if(--indegrees[cur] == 0)
                    queue.add(cur);
        }
        return numCourses == 0;
    }


    //dfs

    /**
     * 借助一个标志列表 flags,用于判断每个节点 i (课程)的状态:
     * 未被 DFS 访问:i == 0;
     * 已被其他节点启动的 DFS 访问:i == -1;
     * 已被当前节点启动的 DFS 访问:i == 1。
     * 对 numCourses 个节点依次执行 DFS,判断每个节点起步 DFS 是否存在环,若存在环直接返回 False。DFS 流程;
     * 终止条件:
     * 当 flag[i] == -1,说明当前访问节点已被其他节点启动的 DFS 访问,无需再重复搜索,直接返回 True。
     * 当 flag[i] == 1,说明在本轮 DFS 搜索中节点 i 被第 2 次访问,即 课程安排图有环 ,直接返回 False。
     * 将当前访问节点 i 对应 flag[i] 置 1,即标记其被本轮 DFS 访问过;
     * 递归访问当前节点 i 的所有邻接节点 j,当发现环直接返回 False;
     * 当前节点所有邻接节点已被遍历,并没有发现环,则将当前节点 flag 置为 -1 并返回 True。
     * 若整个图 DFS 结束并未发现环,返回 true。
     * @param numCourses
     * @param prerequisites
     * @return
     */
    public boolean canFinishDfs(int numCourses, int[][] prerequisites) {
        //邻接表
        List<List<Integer>> adjacency = new ArrayList<>();
        //先往邻接表中加入代表课程数量的固定个数的链表。比如有10个课程,就会加入十个链表。因为每个课程节点都需要维护一个自己的邻接表
        for(int i = 0; i < numCourses; i++)
            adjacency.add(new ArrayList<>());
        //访问标识
        int[] flags = new int[numCourses];
        //为每一个节点在属于他自己的邻接表中添加自己的邻接点
        for(int[] cp : prerequisites)
            adjacency.get(cp[1]).add(cp[0]);
        //进行dfs判断
        /**
         *     c1    c2
         *   /   \  /  \
         *  c8    c3   c5
         *  /      \  / |
         * c9      c4   |
         *   \    /  \  |
         *     c7     c6
         * 这里所有箭头都是向下    dfs会从c1-c8-c9-c7  没有后续邻接点之后,将c7置为-1,表示节点已被某些节点启动的 DFS 访问(针对下一次查询到它是可以直接退出)
         * 回退到c9,发现c9往后查看c7,发现c7 == -1,直接返回true。 c9也被置为 -1.  c8同理
         * 此时进行c1的第二条dfs线路,c1-c3-c4-c6
         * 有环的情况,如果c4-->c6-->c5-->c4   当前就会变成 c1-->c3-->c4-->c6-->c5-->c4 而c4此时依然还只是置为1的状态。 c5-->c4发现 c4 == 1
         * 直接返回false
         */
        for(int i = 0; i < numCourses; i++)
            if(!dfs(adjacency, flags, i)) return false;
        return true;
    }
    private boolean dfs(List<List<Integer>> adjacency, int[] flags, int i) {
        if(flags[i] == 1) return false;
        if(flags[i] == -1) return true;
        flags[i] = 1;
        for(Integer j : adjacency.get(i))
            if(!dfs(adjacency, flags, j)) return false;
        flags[i] = -1;
        return true;
    }
}

偷下懒,都写在代码里面了。

补充知识:

入度: 代表有向无环图中,该节点有多少个节点指向它
出度: 代表有向无环图中,该节点指向多少个节点

** 例子:**

    c1    c4
   /  \  /  \
  c2   c3   c5
        |
       c6

c1的出度为2, c3的入度为2
c5的入度为1 c6的出度为1

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值