图 & 拓扑排序 - [207. 课程表]

图 & 拓扑排序 - 207. 课程表

现在你总共有 n 门课需要选,记为 0n-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。这是不可能的。
说明:

输入的先决条件是由 边缘列表 表示的图形,而不是邻接矩阵。详情请参见图的表示法
你可以假定输入的先决条件中没有重复的边。

提示:

这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
拓扑排序可以通过DFS和BFS实现

一. 知识要点

    1. 图的术语
      • 顶点:图的节点
      • 边:顶点与顶点的连线
      • 相邻顶点:由一条边连接在一起的顶点
      • 度:一个顶点的度是相邻顶点的数量
      • 路径:路径是顶点v1, v2…, vn的一个连续序列, 比如0 1 5 9就是一条路径.
      • 环:一条路径包含相同的顶点两次或者两次以上
      • 无环图:没有环的图,其中 有向无环图 有特殊名称:DAG
      • 连通分支:不相交的连通子图称为图的连通分支。
    2. 图的分类
      • 方向:
        1. 无向图:所有的边都没有方向.
        2. 有向图:边是有方向的
      • 权重:
        1. 带权图:边上有权重
        2. 无权图:边上无权重
    3. 图的表示方式
      • 邻接矩阵
        1. 用二维数组表示顶点之间的连接
        2. 存储方式:二维数组
        3. 画图演示
          邻接矩阵
      • 邻接表
        1. 由图中每个顶点以及和顶点相邻的顶点列表组成。
        2. 存储方式:数组/链表/字典(哈希表)
        3. 画图演示
          邻接表
  1. 拓扑排序方式

    1. BFS - 广度优先搜索
      BFS

    2. DFS - 深度优先搜索
      DFS

二.实现思路

通过 拓扑排序 判断图是否是 有向无环图(DAG)

BFS(广度优先遍历)

  1. 统计课程安排图中每个节点的入度,生成 入度表 indegrees
  2. 借助一个队列queue,将所有入度为 0 的节点入队。
  3. 当 queue 非空时,依次将队首节点出队,在课程安排图中删除此节点 pre:
    • 并不是真正从邻接表中删除此节点 pre,而是将此节点对应所有邻接节点 cur 的入度 -1,即 indegrees[cur] -= 1。
    • 当入度 -1−1后邻接节点 cur 的入度为 0,说明 cur 所有的前驱节点已经被 “删除”,此时将 cur 入队。
  4. 在每次 pre 出队时,执行 numCourses--
    • 若整个课程安排图是有向无环图(即可以安排),则所有节点一定都入队并出队过,即完成拓扑排序。换个角度说,若课程安排图中存在环,一定有节点的入度始终不为 0
    • 因此,拓扑排序出队次数等于课程个数,返回 numCourses == 0 判断课程是否可以成功安排。

DFS(深度优先遍历)

  1. 借助一个标志列表 flags,用于判断每个节点i(课程)的状态:
    1. 未被 DFS 访问:i == 0
    2. 已被其他节点启动的DFS访问:i == -1
    3. 已被当前节点启动的DFS访问:i == 1
  2. numCourses 个节点依次执行 DFS,判断每个节点起步 DFS 是否存在环,若存在环直接返回 False。DFS 流程;
    1. 终止条件:
      • flag[i] == -1,说明当前访问节点已被其他节点启动的 DFS 访问,无需再重复搜索,直接返回 True
      • flag[i] == 1,说明在本轮 DFS 搜索中节点 i 被第 2 次访问,即 课程安排图有环,直接返回 False
    2. 将当前访问节点 i 对应 flag[i] 置 1,即标记其被本轮 DFS 访问过;
    3. 递归访问当前节点 i 的所有邻接节点 j,当发现环直接返回 False
    4. 当前节点所有邻接节点已被遍历,并没有发现环,则将当前节点 flag 置为 -1 并返回 True
  3. 若整个图 DFS 结束并未发现环,返回 True

三.代码实现

BFS

class Solution {

    //numCourses : 选课数量
    //prerequisites : 本质上存的都是图的有向边
    public static boolean canFinish(int numCourses, int[][] prerequisites) {

        //第一部分:初始化数据
        int[] indegrees = new int[numCourses];
        //初始化每个顶点的入度。
        //prerequisites = {{1, 0}, {3, 0}, {2, 1},{3, 2}};
        for(int[] cp : prerequisites) {
            //第一次循环:cp={1, 0};cp[0]=1
            //第二次循环:cp={3, 0};cp[0]=3
            //第三次循环:cp={2, 1};cp[0]=2
            //第四次循环:cp={3, 2};cp[0]=3
            //每次加一 : 因为cp[0]是被指向的节点,indegrees[cp[0]]代表第几个节点。cp[0]每重复一次,就说明这个节点入度多了一次。
            indegrees[cp[0]]++;
        }
        //循环完毕得出节点入度数组 : indegrees = { 0 , 1 , 1 , 2 }
        LinkedList<Integer> queue = new LinkedList<>();
        //将入度为0的节点添加到queue链表中 : queue = ["0"]
        for(int i = 0; i < numCourses; i++){
            if(indegrees[i] == 0) queue.addLast(i);
        }


        //第二部分:逻辑处理
        //如果 存放入度为0的节点的链表queue 非空,则进入循环
        while(!queue.isEmpty()) {
            //依次将队首节点出队,pre就是首节点
            //第一次循环: pre = 0
            Integer pre = queue.removeFirst();
            //选课数量减一
            numCourses--;
            //prerequisites = {{1, 0}, {3, 0}, {2, 1},{3, 2}};
            for(int[] req : prerequisites) {
                //通过队首节点pre,筛选到与队首节点所有邻接节点之间的连接线
                if(req[1] != pre) {
                    continue;
                }
                //第一次循环 : req 为 1 和 3
                //队首节点的入度减一
                //如果节点1 和 3 的入度为 0 ,则将 1 和 3 再次加入到入度为0的节点的链表queue
                if(--indegrees[req[0]] == 0) {
                    queue.add(req[0]);
                }
            }
        }
        //出队次数等于课程数的话,就可以选课成功
        return numCourses == 0;
    }


    public static void main(String[] args) {
        int[][] a = {{1,0}, {3, 0}, {2, 1},{3, 2}};
        boolean b = canFinish(4, a);
        System.out.println(b);
    }
}

DFS

class Solution {
    public static boolean canFinish(int numCourses, int[][] prerequisites) {
        //构建邻接矩阵
        int[][] adjacency = new int[numCourses][numCourses];
        //设置一个flags数组,记录节点状态
        //未被 DFS 访问:i == 0;
        //已被其他节点启动的DFS访问:i == -1;
        //已被当前节点启动的DFS访问:i == 1。
        int[] flags = new int[numCourses];
        //prerequisites = {{1,0}, {3, 0}, {2, 1},{3, 2}};
        for(int[] cp : prerequisites){
            //根据prerequisites,初始化矩阵
            adjacency[cp[1]][cp[0]] = 1;
        }
        //对 numCourses 个节点依次执行 DFS
        for(int i = 0; i < numCourses; i++){
            if(!dfs(adjacency, flags, i)) return false;
        }
        //若整个图 DFS 结束并未发现环,返回 TrueTrue
        return true;
    }
    private static boolean  dfs(int[][] adjacency, int[] flags, int i) {
        //终止条件:
        // 当 flag[i] == 1,说明在本轮 DFS 搜索中节点 i 被第 2 次访问,即 课程安排图有环,直接返回 False。
        if(flags[i] == 1) return false;
        // 当 flag[i] == -1,说明当前访问节点已被其他节点启动的 DFS 访问,无需再重复搜索,直接返回 True。
        if(flags[i] == -1) return true;
        // 将当前访问节点 i 对应 flag[i] 置 1,即标记其被本轮 DFS 访问过;
        flags[i] = 1;
        for(int j = 0; j < adjacency.length; j++) {
            //递归访问当前节点 i 的所有邻接节点 j,当发现环直接返回 False;
            if(adjacency[i][j] == 1 && !dfs(adjacency, flags, j)) {
                return false;
            }
        }
        //当前节点所有邻接节点已被遍历,并没有发现环,则将当前节点 flag 置为 -1−1 并返回 True
        flags[i] = -1;
        return true;
    }

    public static void main(String[] args) {
        int[][] a = {{1,0}, {3, 0}, {2, 1},{3, 2}};
        boolean b = canFinish(4, a);
        System.out.println(b);
    }
}

:上述代码中二维数组a,表示的图应该是
在这里插入图片描述

发布了96 篇原创文章 · 获赞 26 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq919694688/article/details/103148269