0
点赞
收藏
分享

微信扫一扫

拓扑排序 Topological sorting



拓扑排序 Topological sorting

  • [207. 课程表](https://leetcode.cn/problems/course-schedule/)
  • [210. 课程表 II](https://leetcode.cn/problems/course-schedule-ii/)
  • [6163. 给定条件下构造矩阵](https://leetcode.cn/problems/build-a-matrix-with-conditions/)
  • [310. 最小高度树](https://leetcode.cn/problems/minimum-height-trees/)
  • 329. 矩阵中的最长递增路径
  • 802. 找到最终的安全状态
  • [851. 喧闹和富有](https://leetcode.cn/problems/loud-and-rich/)
  • 913. 猫和老鼠
  • 1203. 项目管理
  • [1462. 课程表 IV](https://leetcode.cn/problems/course-schedule-iv/)
  • 1591. 奇怪的打印机 II
  • 1632. 矩阵转换后的秩
  • 1719. 重构一棵树的方案数
  • 1728. 猫和老鼠 II
  • 1786. 从第一个节点出发到最后一个节点的受限路径数
  • 1857. 有向图中最大颜色值
  • 1916. 统计为蚁群构筑房间的不同顺序
  • 1976. 到达目的地的方案数
  • 2050. 并行课程 III
  • 2115. 从给定原材料中找到所有可以做出的菜
  • 2127. 参加会议的最多员工数
  • 2192. 有向无环图中一个节点的所有祖先
  • 2246. 相邻字符不同的最长路径
  • 2328. 网格图中递增路径的数目
  • 2360. 图中的最长环
  • LCP 21. 追逐游戏
  • 剑指 Offer II 112. 最长递增路径
  • 剑指 Offer II 113. 课程顺序
  • 剑指 Offer II 114. 外星文字典
  • 剑指 Offer II 115. 重建序列



给定一个包含 n 个节点的有向图 G,给出它的节点编号的一种排列,如果满足:


对于图 G 中的任意一条有向边 (u, v),u 在排列中都出现在 v 的前面。


那么称该排列是图 G 的「拓扑排序」。

在工作和生活中,不同任务之间通常会存在某些依赖关系,会对它们的执行顺序形成部分约束。

在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列。且该序列必须满足下面两个条件:

  • 每个顶点出现且只出现一次。
  • 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。

有向无环图(DAG)才有拓扑排序,非 DAG 图没有拓扑排序一说。

对于有向图的某个结点,指向它的边的数量叫做入度;从它发出的边的数量称为出度

拓扑排序 Topological sorting_leetcode


它是一个 DAG 图,拓扑排序比较常用的方法:

  • 从 DAG 图中选择一个 没有前驱(即入度为 0)的顶点并输出。
  • 从图中删除该顶点和所有以它为起点的有向边。
  • 重复 1 和 2 直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。

    拓扑排序后的结果是 [ 1, 2, 4, 3, 5 ]。

通常,一个有向无环图可以有一个或多个拓扑排序序列。

图论

207. 课程表

考虑拓扑排序中最前面的节点,该节点一定不会有任何入边,也就是它没有任何的先修课程要求。将一个节点加入答案中后,就可以移除它的所有出边,代表着它的相邻节点少了一门先修课程的要求。如果某个相邻节点变成了「没有任何入边的节点」,那么就代表着这门课可以开始学习了。按照这样的流程,不断地将没有入边的节点加入答案,直到答案中包含所有的节点(得到了一种拓扑排序)或者不存在没有入边的节点(图中包含环)。
从入度思考(从前往后排序), 入度为 0 的节点在拓扑排序中一定排在前面, 然后删除和该节点对应的边, 迭代寻找入度为 0 的节点。
从出度思考(从后往前排序), 出度为 0 的节点在拓扑排序中一定排在后面, 然后删除和该节点对应的边, 迭代寻找出度为 0 的节点。

class Solution:
    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
    
    邻接表:用哈希表记录依赖关系(也可以用二维矩阵,但有点大)
    key:课号
    value:依赖这门课的后续课(数组)


        d = defaultdict(list)
        indeg = [0] * numCourses
        for x, y in prerequisites:
            d[y].append(x)
            indeg[x] += 1       
        q = [i for i, x in enumerate(indeg) if x == 0]
        i = 0
        while i < len(q):
            for y in d[q[i]]:
                indeg[y] -= 1
                if indeg[y] == 0: q.append(y)
            i += 1            
        return len(q) == numCourses

课程安排图是否是 有向无环图(DAG)。即课程间规定了前置条件,但不能构成任何环路,否则课程前置条件将不成立。
通过 拓扑排序 判断此课程安排图是否是 有向无环图(DAG) 。 拓扑排序原理: 对 DAG 的顶点进行排序,使得对每一条有向边 (u, v),均有 u(在排序记录中)比 v 先出现。亦可理解为对某点 v 而言,只有当 v 的所有源点均出现了,v 才能出现。
通过课程前置条件列表 prerequisites 可以得到课程安排图的 邻接表 adjacency,以降低算法时间复杂度,以下两种方法都会用到邻接表。

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

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        int[] indegrees = new int[numCourses];
        List<List<Integer>> adjacency = new ArrayList<>();
        Queue<Integer> q= new LinkedList<>();
        for(int i = 0; i < numCourses; i++)
            adjacency.add(new ArrayList<>());
        // Get the indegree and adjacency of every course.
        for(int[] cp : prerequisites) {
            indegrees[cp[0]]++;
            adjacency.get(cp[1]).add(cp[0]);
        }
        // Get all the courses with the indegree of 0.
        for(int i = 0; i < numCourses; i++)
            if(indegrees[i] == 0) q.add(i);
        // BFS TopSort.
        while(!q.isEmpty()) {
            int pre = q.poll();
            numCourses--;
            for(int cur : adjacency.get(pre))
                if(--indegrees[cur] == 0) q.add(cur);
        }
        return numCourses == 0;
    }
}

方法二:深度优先遍历

原理是通过 DFS 判断图中是否有环。
借助一个标志列表 flags,用于判断每个节点 i (课程)的状态:

未被 DFS 访问:i == 0;
已被其他节点启动的 DFS 访问:i == -1;
已被当前节点启动的 DFS 访问:i == 1。
对 numCourses 个节点依次执行 DFS,判断每个节点起步 DFS 是否存在环,若存在环直接返回 FalseFalseFalse。DFS 流程;
终止条件:
当 flag[i] == -1,说明当前访问节点已被其他节点启动的 DFS 访问,无需再重复搜索,直接返回 TrueTrueTrue。
当 flag[i] == 1,说明在本轮 DFS 搜索中节点 i 被第 222 次访问,即 课程安排图有环 ,直接返回 FalseFalseFalse。
将当前访问节点 i 对应 flag[i] 置 111,即标记其被本轮 DFS 访问过;
递归访问当前节点 i 的所有邻接节点 j,当发现环直接返回 FalseFalseFalse;
当前节点所有邻接节点已被遍历,并没有发现环,则将当前节点 flag 置为 −1-1−1 并返回 TrueTrueTrue。
若整个图 DFS 结束并未发现环,返回 TrueTrueTrue。

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        List<List<Integer>> adjacency = new ArrayList<>();
        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]);
        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;
    }
}

210. 课程表 II

class Solution:
    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        d = defaultdict(list)
        indeg = [0] * numCourses
        for a, b in prerequisites:
            d[b].append(a)
            indeg[a] += 1
        order = []
        q = deque(i for i, x in enumerate(indeg) if x == 0)
        while q:
            x = q.popleft()
            order.append(x)
            for y in d[x]:
                indeg[y] -= 1
                if indeg[y] == 0: q.append(y)
        return order if len(order) == numCourses else []

6163. 给定条件下构造矩阵

class Solution:
    def buildMatrix(self, k: int, rowConditions: List[List[int]], colConditions: List[List[int]]) -> List[List[int]]:
        def toposort(edges):
            g = [[] for _ in range(k)]
            indeg = [0] * k
            for x, y in edges:
                g[x - 1].append(y - 1)  # 顶点编号从 0 开始,方便计算
                indeg[y - 1] += 1
            order = []
            q = deque(i for i, d in enumerate(indeg) if d == 0)
            while q:
                x = q.popleft()
                order.append(x)
                for y in g[x]:
                    indeg[y] -= 1
                    if indeg[y] == 0: q.append(y)
            return order if len(order) == k else None

        if (row := toposort(rowConditions)) is None or (col := toposort(colConditions)) is None: return []
        pos = {x: i for i, x in enumerate(col)}
        ans = [[0] * k for _ in range(k)]
        for i, x in enumerate(row):
            ans[i][pos[x]] = x + 1
        return ans

310. 最小高度树

329. 矩阵中的最长递增路径

802. 找到最终的安全状态

851. 喧闹和富有

class Solution:
    def loudAndRich(self, richer: List[List[int]], quiet: List[int]) -> List[int]:

        def dfs(x):
            if ans[x] != -1: return 
            ans[x] = x
            for other in d[x]:
                dfs(other)
                if quiet[ans[other]] < quiet[ans[x]]:
                    ans[x] = ans[other]

        n = len(quiet)
        ans, d = [-1] * n, defaultdict(list)
        for a, b in richer: d[b].append(a)
        
        for i in range(n): dfs(i)
        return ans

        n = len(quiet)
        g = defaultdict(list)
        indeg = [0] * n
        for a, b in richer:
            g[a].append(b)
            indeg[b] += 1

        ans = list(range(n)) # 初始化为自身
        q = deque(i for i, x in enumerate(indeg) if x == 0)
        while q:
            x = q.popleft()
            for y in g[x]:
                # x 比 y 更有钱,如果更安静,更新 ans[y]
                if quiet[ans[x]] < quiet[ans[y]]:
                    ans[y] = ans[x]
                indeg[y] -= 1
                if indeg[y] == 0:
                    q.append(y)
        return ans

913. 猫和老鼠

1203. 项目管理

1462. 课程表 IV

class Solution:
    def checkIfPrerequisite(self, numCourses: int, prerequisites: List[List[int]], queries: List[List[int]]) -> List[bool]:
        f = defaultdict(set)
        for u, v in prerequisites:
            f[u].add(v)
            
        ans = []
        visited = [False] * n

        def dfs(c):
            if visited[c]: return f[c]
            newpre = set()
            for pre in f[c]:
                newpre.update(dfs(pre))
            f[c].update(newpre)  # 将所有的祖先先修课程全都直接添加到该课程的先修里面
            visited[c] = True
            return f[c]

        for i in range(n):
            dfs(i)

        return [True if v in f[u] else False for u, v in queries]

        g = defaultdict(list)
        indeg = [0] * numCourses
        for u, v in prerequisites:
            g[u].append(v)
            indeg[v] += 1
        q = [i for i, x in enumerate(indeg) if x == 0]

        f = defaultdict(set) # v 的前导课程
        while q:
            u = q.pop()
            for v in g[u]:
                f[v].add(u)
                f[v].update(f[u])
                indeg[v] -= 1
                if not indeg[v]:
                    q.append(v)
        
        return [True if u in f[v] else False for u, v in queries]

1591. 奇怪的打印机 II

1632. 矩阵转换后的秩

1719. 重构一棵树的方案数

1728. 猫和老鼠 II

1786. 从第一个节点出发到最后一个节点的受限路径数

1857. 有向图中最大颜色值

1916. 统计为蚁群构筑房间的不同顺序

1976. 到达目的地的方案数

2050. 并行课程 III

2115. 从给定原材料中找到所有可以做出的菜

2127. 参加会议的最多员工数

2192. 有向无环图中一个节点的所有祖先

2246. 相邻字符不同的最长路径

2328. 网格图中递增路径的数目

2360. 图中的最长环

LCP 21. 追逐游戏

剑指 Offer II 112. 最长递增路径

剑指 Offer II 113. 课程顺序

剑指 Offer II 114. 外星文字典

剑指 Offer II 115. 重建序列


举报

相关推荐

0 条评论