今天是算法和数据结构专题的第32篇文章,我们来聊聊拓扑排序的问题。
拓扑排序是图论当中一个非常简单也非常常用的算法,它有很多的功能。它可以用来检测有向图当中是否存在环,也可以用来解决存在依赖的调度问题。下面我们就来看看这个算法的庐山真面目吧。
算法场景
拓扑排序是英文音译,它的英文原文是Topological Sorting,是一个比较抽象的概念,没有很信达雅的翻译。它指的是一个DAG(Directed Acyclic Graph)即有向图所有顶点满足一定条件的线性序列。也就是说图中的这些顶点的排序之间存在一定的逻辑结构和顺序结构,是这两种拧在一起的一个抽象的概念。
那么这些顶点的排序之间应该满足什么样的条件呢?
其实很简单只有两点:
每个点都只出现一次,这个不用解释了 如果存在一条从A指向B的边,那么A点在序列中出现的顺序应该在B点之前。关于这点简单解释一下,我们可以把有向边看成一条调度上的依赖。A指向B,即B依赖A,那么显然A应该出现在B前面。就好像淘米 -> 煮饭,我们应该先淘米才能煮饭,煮饭依赖淘米。
比如上图当中1 2 4 3 5就是一个合法的拓扑排序,这个序列满足上面两条性质。
算法原理
那么我们怎么得到这个拓扑排序呢?
其实原理非常简单,就是一个数组的事情。首先,我们用一个数组记录每一个点的入度。所谓的入度也就是有多少点指向它,比如上图当中1号点的入度为0,4号点的入度为2,因为它有1和2两个点指向它。
我们要做的就是根据入度一个点一个点的选择,根据前面说的性质,如果一个点的入度不为0,那么它显然不能被选择。因为至少还有一个点应该在它的前面,既然如此,反过来说也就是我们只能选择入度为0的点。如果所有点的入度都不为0呢?不要问,图中肯定有环,不然一定可以找到一个入度为0的点。关于这点有严谨的证明,但我们也没必要证明了,仔细想想就能想明白。
我们选中了入度为0的点之后呢?之后我们需要把它连出去的边全部删掉,我们一样从调度依赖来思考。比如我们想要做寿司也想要做饭团,这两者都依赖于煮饭。现在饭煮好了,这两者的依赖已经完成了,它们应该不受任何限制了。所以我们要把煮饭连向做饭团和做寿司的边去掉,也就是把依赖去掉。去掉边体现在做饭团和做寿司的入度减一,也就是它们上游的依赖少了一个。
整个流程串起来就是拓扑排序的算法了,怎么样是不是很简单呢?
但是还有一个小问题,根据这样我们得到的序列是的吗?如果存在多个入度为0的点怎么办,我们该选哪一个?
显然拓扑排序的情况可能是不的,但是我们是否要获取所有的情况这一点就要根据实际使用的情况来确定了,一般来说我们只需要一个合法的序列就可以了,如果需要得到所有的拓扑排序也不复杂,我们可以将它看成一个带条件限制的搜索问题,搜索一下所有的可能性就OK了。
代码实现
后,我们来看下代码,真的是史诗级的简单:
paths = [[], [2, 4], [3, 4], [5], [3, 5], []]
indegree = [ for _ in range(6)]
for u in range(6):
for v in paths[u]:
indegree[v] += 1
topological = set()
for i in range(5):
for u in range(1, 6):
if u not in topological and indegree[u] == :
topological.add(u)
for v in paths[u]:
indegree[v] -= 1
print(topological)
今天的文章到这里就结束了,如果喜欢本文的话,请来一波素质三连,给我一点支持吧(关注、在看、点赞)。