6-拓扑排序

一、什么是拓扑排序

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

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

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

重点:

  • 拓扑排序是专门应用于有向图的算法;
  • BFS 的写法就叫拓扑排序;拓扑排序的结果不唯一;
  • 拓扑排序的一个附加效果是:能够顺带检测有向图中是否存在环,这个知识点非常重要
  • ps:无向图中,检测是否成环,使用的数据结构是并查集,例如 LeetCode 684. 冗余连接

例如,下面这个图:
图1

它是一个 DAG 图,那么如何写出它的拓扑排序呢?这里说一种比较常用的方法:

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

图2


二、LeetCode 练习

210 课程表

import queue


class Vertex:
    def __init__(self, val):
        self.id = val
        self.prev = {}
        self.neighbors = {}

    def add_nbr(self, nbr, w=None):
        self.neighbors[nbr] = w

    def get_nbr(self):
        return self.neighbors.keys()

    def add_prev(self, prev, w=None):
        self.prev[prev] = w

    def get_prev(self):
        return self.prev.keys()


class Graph:
    def __init__(self):
        self.vertex_num = 0
        self.vertexs = {}

    def __iter__(self):
        return iter(self.vertexs.values())

    def add_vertex(self, val):
        if val not in self.vertexs:
            self.vertexs[val] = Vertex(val)
            self.vertex_num += 1

    def add_edge(self, f, t, w=None):
        if f not in self.vertexs:
            self.add_vertex(f)
        if t not in self.vertexs:
            self.add_vertex(t)
        self.vertexs[f].add_nbr(self.vertexs[t], w)
        self.vertexs[t].add_prev(self.vertexs[f], w)

    def get_vertex(self, val):
        if val in self.vertexs:
            return self.vertexs[val]


class Solution:
    @classmethod
    def find_order(cls, num_courses: int, prerequisites: list) -> list:
        """方法一:
        1、遍历所有关联的课程,构造图
        2、循环遍历图,将无后继的叶子节点放入结果列表中,同时更新与叶子节点相连的前驱节点的邻接点。
        如果在一次遍历过程中没有找到叶子节点,说明剩下的都是成环的节点,不满足要求,直接返回
        """
        ret = []
        my_graph = Graph()
        for pre_req in prerequisites:
            my_graph.add_edge(*pre_req)

        while my_graph.vertex_num > 0:
            cur_v = []
            for v in my_graph:
                if v.get_nbr():
                    continue
                cur_v.append(v)
                ret.append(v.id)
                for prev in v.get_prev():
                    prev.neighbors.pop(v)
            if not cur_v:  # 如果有向图中,存在环,拓扑排序不能继续得到入度值为0的节点,退出循环,此时图中存在没有遍历到的节点,说明图中存在环
                return []
            for v in cur_v:
                my_graph.vertexs.pop(v.id)
                my_graph.vertex_num -= 1
        return ret + list(set(range(num_courses)).difference(set(ret)))

    @classmethod
    def find_order_pro(cls, num_courses: int, prerequisites: list) -> list:
        """方法一优化:将循环遍历查找叶子节点的方法修改为广度优先搜索。"""
        ret = []
        my_graph = Graph()
        for pre_req in prerequisites:
            my_graph.add_edge(*pre_req)

        # 拿到无后继节点的叶子节点,入队
        q = queue.Queue()
        in_grp_courses = []
        for v in my_graph:
            in_grp_courses.append(v.id)
            if v.get_nbr():
                continue
            q.put(v)

        # 广度优先搜索,更新前驱节点的后继节点的同时,判断前驱节点是否无后继节点了,如果没有,则前驱节点入队
        while not q.empty():
            cur_v = q.get()
            ret.append(cur_v.id)
            for prev in cur_v.get_prev():
                prev.neighbors.pop(cur_v)
                if not prev.get_nbr():
                    q.put(prev)

        # 广度搜索完成后,如果有节点成环,则不会进去返回列表中
        ret = ret + list(set(range(num_courses)).difference(set(in_grp_courses)))
        if len(ret) != num_courses:
            return []
        return ret

    @classmethod
    def find_order_by_list(cls, num_courses: int, prerequisites: list) -> list:
        """方法二:更轻量级的图:用集合表示邻接表,用数字记录后继节点数量。"""

        adj_list = {i: [set(), 0] for i in range(num_courses)}
        for pre_req in prerequisites:
            # 要修课程pre_req[0],需要先修完课程pre_req[1]。所以课程pre_req[0]的后继节点数+1;
            # 而课程pre_req[1]的邻接表中需要保存课程pre_req[0],以便课程pre_req[1]进入结果列表后更新前驱课程课程pre_req[0]的后继节点数。
            # 当课程pre_req[0]的后继节点数更新到0时,课程pre_req[0]也可以进入结果列表。
            adj_list[pre_req[0]][1] += 1
            adj_list[pre_req[1]][0].add(pre_req[0])
        q = []
        for i, v in adj_list.items():
            if v[1] == 0:
                q.append(i)
        ret = []
        while q:
            cur_course = q.pop()
            ret.append(cur_course)
            for prev in adj_list.get(cur_course)[0]:
                adj_list.get(prev)[1] -= 1
                if adj_list.get(prev)[1] == 0:
                    q.insert(0, prev)
        if len(ret) != num_courses:
            return []
        return ret


if __name__ == '__main__':
    res = Solution.find_order_by_list(2, [[1,0]])
    print(res)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一个两个四个三

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值