一、什么是拓扑排序
在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列。且该序列必须满足下面两个条件:
- 每个顶点出现且只出现一次。
- 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说。
重点:
- 拓扑排序是专门应用于有向图的算法;
- BFS 的写法就叫拓扑排序;拓扑排序的结果不唯一;
- 拓扑排序的一个附加效果是:能够顺带检测有向图中是否存在环,这个知识点非常重要
- ps:无向图中,检测是否成环,使用的数据结构是并查集,例如 LeetCode 684. 冗余连接
例如,下面这个图:
它是一个 DAG 图,那么如何写出它的拓扑排序呢?这里说一种比较常用的方法:
- 从 DAG 图中选择一个 没有前驱(即入度为0)的顶点并输出。
- 从图中删除该顶点和所有以它为起点的有向边。
- 重复 1 和 2 直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。
二、LeetCode 练习
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)