【问题标题】:deceptively simple implementation of topological sorting in pythonpython中拓扑排序的看似简单的实现
【发布时间】:2022-04-08 12:15:23
【问题描述】:

here 中提取,我们得到了一个最小的迭代 dfs 例程,我称它为最小,因为您几乎无法进一步简化代码:

def iterative_dfs(graph, start, path=[]):
    q = [start]
    while q:
        v = q.pop(0)
        if v not in path:
            path = path + [v]
            q = graph[v] + q

    return path

graph = {
    'a': ['b', 'c'],
    'b': ['d'],
    'c': ['d'],
    'd': ['e'],
    'e': []
}
print(iterative_dfs(graph, 'a'))

这是我的问题,您如何将这个例程转换为拓扑排序方法,其中例程也变得“最小”?我看过这个video,这个想法非常聪明,所以我想知道是否可以将相同的技巧应用到上面的代码中,这样topological_sort 的最终结果也变得“最小”。

不要求拓扑排序的版本不是对上述例程的微小修改,我已经见过其中的几个。问题不是“我如何在 python 中实现拓扑排序”,而是找到上述代码的最小可能调整集以成为topological_sort

补充意见

作者在原文中说:

不久前,我读到 Guido van Rossen 的一个图形实现 看似简单。现在,我坚持纯python最小系统 以最少的复杂性。这个想法是能够探索 算法。稍后,您可以细化和优化代码,但您将 可能想在编译语言中执行此操作。

这个问题的目标不是优化iterative_dfs,而是想出一个从它派生的最小版本的拓扑排序(只是为了了解更多关于图论算法的信息)。事实上,我想一个更普遍的问题可能是给定一组最小算法,{iterative_dfsrecursive_dfsiterative_bfsrecursive_dfs},它们的拓扑排序推导是什么?虽然这会使问题变得更冗长/复杂,但从 iterative_dfs 中找出 topological_sort 就足够了。

【问题讨论】:

  • @TomKarzes 现在理解你的意思,这完全有道理,好主意!您是否认为在遍历之前预处理将 deps 列表转换为集合的图形过于“昂贵”,因此您不需要将两个结构都保留在图形中。假设应用程序要处理的图形类型可能是 {graph_many_nodes}x{all_nodes_have_many_deps, few_nodes_many_deps} 之类的笛卡尔积,因此值得优化。无论如何,问题更多的是关于最少的代码(尽可能简单),因此在这种情况下,解决方案是否快速无关紧要。
  • 还有path=[]...只是不要这样做...
  • @user3012759 你的意思是把它作为一个局部变量而不是一个参数?看看我在帖子中的原始article,如果你仔细想想,作者选择path=[]作为参数的原因是这样迭代和递归版本的接口都可以是同样,如果您将其作为本地 var,您将在这一点上失败。
  • @BPL - 在这种情况下,它确实按预期工作,但这只是因为另一个技术性。 path = ... 的行将新列表分配给本地 var 路径并返回,但如果将来有人进入并更改为 path.append(v) 执行“同样的事情”,你会突然得到 path 保持调用之间的值 - 通常你不想将可变变量作为默认参数放在 python 中
  • @ShihabShahriar 我明白你为什么要问......老实说,我在决定接受哪个答案时遇到了很多困难,它们都非常适合我。我之所以接受 Blckknght 主要是因为点赞的数量,它是第一个发布的,它对公众更有用,因为它提供了非常好的见解。另一方面,如果我们严格坚持我提出的问题,我认为您的答案更合适的 ...所以...我不知道,也许我选错了两个答案真的很好,但我需要选择一个....

标签: python algorithm graph-theory depth-first-search topological-sort


【解决方案1】:

将 DFS 的迭代实现转换为拓扑排序并不容易,因为需要进行的更改使用递归实现更自然。但是你仍然可以这样做,它只需要你实现自己的堆栈。

首先,这是您的代码稍微改进的版本(它更高效,也不是更复杂):

def iterative_dfs_improved(graph, start):
    seen = set()  # efficient set to look up nodes in
    path = []     # there was no good reason for this to be an argument in your code
    q = [start]
    while q:
        v = q.pop()   # no reason not to pop from the end, where it's fast
        if v not in seen:
            seen.add(v)
            path.append(v)
            q.extend(graph[v]) # this will add the nodes in a slightly different order
                               # if you want the same order, use reversed(graph[v])

    return path

以下是我如何修改该代码以进行拓扑排序:

def iterative_topological_sort(graph, start):
    seen = set()
    stack = []    # path variable is gone, stack and order are new
    order = []    # order will be in reverse order at first
    q = [start]
    while q:
        v = q.pop()
        if v not in seen:
            seen.add(v) # no need to append to path any more
            q.extend(graph[v])

            while stack and v not in graph[stack[-1]]: # new stuff here!
                order.append(stack.pop())
            stack.append(v)

    return stack + order[::-1]   # new return value!

我用“这里的新东西”评论的部分是在您向上移动时计算顺序的部分。它检查找到的新节点是否是前一个节点(位于堆栈顶部)的子节点。如果不是,它会弹出堆栈顶部并将值添加到order。在我们进行 DFS 时,order 将从最后一个值开始以相反的拓扑顺序排列。我们在函数的末尾反转它,并将它与堆栈上的剩余值连接起来(方便地,它们已经按正确的顺序排列了)。

因为这段代码需要检查v not in graph[stack[-1]]很多次,如果graph字典中的值是集合而不是列表,效率会高很多。图通常不关心其边的保存顺序,因此进行此类更改不会导致大多数其他算法出现问题,尽管生成或更新图的代码可能需要修复。如果您打算扩展您的图形代码以支持加权图形,您可能最终将列表更改为字典,从节点映射到权重,这对于此代码同样有效(字典查找是 O(1) 只是像集合查找)。或者,如果graph不能直接修改,我们可以自己构建我们需要的集合。

作为参考,这里是 DFS 的递归版本,并对其进行了修改以进行拓扑排序。所需的修改确实非常小:

def recursive_dfs(graph, node):
    result = []
    seen = set()

    def recursive_helper(node):
        for neighbor in graph[node]:
            if neighbor not in seen:
                result.append(neighbor)     # this line will be replaced below
                seen.add(neighbor)
                recursive_helper(neighbor)

    recursive_helper(node)
    return result

def recursive_topological_sort(graph, node):
    result = []
    seen = set()

    def recursive_helper(node):
        for neighbor in graph[node]:
            if neighbor not in seen:
                seen.add(neighbor)
                recursive_helper(neighbor)
        result.insert(0, node)              # this line replaces the result.append line

    recursive_helper(node)
    return result

就是这样!一行被删除,类似的一行被添加到不同的位置。如果您关心性能,您可能也应该在第二个辅助函数中使用result.append,并在顶级recursive_topological_sort 函数中使用return result[::-1]。但是使用insert(0, ...) 是一个更小的变化。

另外值得注意的是,如果您想要整个图的拓扑顺序,则不需要指定起始节点。实际上,可能没有一个节点可以让您遍历整个图形,因此您可能需要进行多次遍历才能到达所有内容。在迭代拓扑排序中实现这一点的一种简单方法是将q 初始化为list(graph)(所有图键的列表),而不是只有一个起始节点的列表。对于递归版本,如果 seen 中还没有,则将调用 recursive_helper(node) 替换为调用图中每个节点上的辅助函数的循环。

【讨论】:

  • 在迭代实现中,变量q实际上是一个栈。
  • @Saurabh 是的,这是真的。原始问题代码中的 q 变量也是一个堆栈,尽管它的效率要低得多,因为它从一开始就推送和弹出,而不是列表具有高效操作的末尾。这就是使 DFS 成为 DFS 的原因。常规(LIFO)队列将提供广度优先遍历。不过,FIFO 队列并不是我们正在讨论的代码值得注意的地方。拓扑排序代码中名为stack的栈替换了递归版本的调用栈。
  • 在递归DFS实现中,result = []应该是result = [node]
【解决方案2】:

我的想法基于两个关键观察:

  1. 不要从堆栈中弹出下一项,保留它以模拟堆栈展开。
  2. 不要将所有子项推入堆栈,只需推一个即可。

这两者都帮助我们像递归 dfs 一样遍历图。正如这里的另一个答案所指出的,这对于这个特定问题很重要。其余的应该很容易。

def iterative_topological_sort(graph, start,path=set()):
    q = [start]
    ans = []
    while q:
        v = q[-1]                   #item 1,just access, don't pop
        path = path.union({v})  
        children = [x for x in graph[v] if x not in path]    
        if not children:              #no child or all of them already visited
            ans = [v]+ans 
            q.pop()
        else: q.append(children[0])   #item 2, push just one child

    return ans

q 这是我们的堆栈。在主循环中,我们从堆栈中“访问”当前节点v。 'access',而不是'pop',因为我们需要能够再次回到这个节点。我们找出当前节点的所有未访问的子节点。并且只将第一个推入堆栈 (q.append(children[0])),而不是全部推到一起。同样,这正是我们使用递归 dfs 所做的。

如果没有找到符合条件的孩子(if not children),我们已经访问了它下面的整个子树。所以它已经准备好被推送到ans。这是我们真正弹出它的时候。

(当然,这远非最佳性能。有几种相当简单的方法可以提高性能,但为了简单起见,我忽略了这些。)

【讨论】:

  • 这是一个很棒的方法!在概念上非常接近“纯”dfs!
  • @StevenPenny,我不同意。 “有向图的拓扑排序是其顶点的线性排序,因此对于从顶点 u 到顶点 v 的每条有向边 uv,u 在排序中排在 v 之前。” wiki link
  • 在给定的示例中,我的解决方案输出:['a', 'c', 'b', 'd', 'e'],我认为这是所需的排序,也是接受的解决方案的排序。
【解决方案3】:

我对此很陌生,但是无论您从图中的哪个位置开始,基于 DFS 的拓扑排序都不应该起作用吗?当前的解决方案(在撰写本文时)仅针对示例图中的特定起点遍历整个图。 (虽然我还没有完全考虑清楚,但似乎在遇到没有邻居可访问的顶点时会出现问题。如果算法在遍历图中的所有其他顶点之前命中了这样的节点,那么结果会被截断。 )

虽然它不像 OP 可能想要的那么简单,但以下是使用 DFS 的迭代拓扑排序,无论探索的顶点顺序如何,它都能正常工作。

```
from collections import deque

def iterative_top_sort(graph):
    result = deque() #using deque because we want to append left
    visited = set()

    #the first entry to the stack is a list of the vertices in the 
    #graph. 

    stack = [[key for key in graph]] #we want the stack to hold lists

    while stack:
      for i in stack[-1]: 
        if i in visited and i not in result: 
          result.appendleft(i)
        if i not in visited:
          visited.add(i)
          #add the vertex's neighbors to the stack
          stack.append(graph[i]) 
          break
      else: 
        stack.pop() 

    return result
```

【讨论】:

    【解决方案4】:

    我也试图简化这一点,所以我想出了这个:

    from collections import deque
    
    def dfs(graph, source, stack, visited):
        visited.add(source)
    
        for neighbour in graph[source]:
            if neighbour not in visited:
                dfs(graph, neighbour, stack, visited)
        
        stack.appendleft(source)
    
    def topological_sort_of(graph):
        stack = deque()
        visited = set()
    
        for vertex in graph.keys():
            if vertex not in visited:
                dfs(graph, vertex, stack, visited)
    
        return stack
    
    if __name__ == "__main__":
        graph = {
            0: [1, 2],
            1: [2, 5],
            2: [3],
            3: [],
            4: [],
            5: [3, 4],
            6: [1, 5],
        }
    
        topological_sort = topological_sort_of(graph)
        print(topological_sort)
    

    函数dfs(深度优先搜索)用于为图中的每个顶点创建完成时间的堆栈。这里的完成时间意味着首先推入堆栈的元素是第一个完全探索其所有邻居的顶点(没有其他未访问的邻居可从该顶点探索)并且最后一个推入堆栈的元素是最后一个顶点它的所有邻居都已被充分探索。

    堆栈现在只是拓扑排序。

    visited 使用Python 集提供了常量成员资格检查,并且使用deque 作为堆栈还提供了常量时间左插入。

    高级想法的灵感来自 CLRS [1]。

    [1] Cormen,Thomas H.,等人。算法简介。麻省理工学院出版社,2009 年。

    【讨论】:

      【解决方案5】:

      鉴于您的示例图:

      a -->-- b -->-- d -->-- e
       \             /
        -->-- c -->--
      

      我们需要实现图表,您已经使用“父到子”完成了该操作:

      graph = {
         'a': ['b', 'c'],
         'b': ['d'],
         'c': ['d'],
         'd': ['e'],
         'e': [],
      }
      

      但您还提供了start 参数。在拓扑排序的上下文中, 如果您提供a,则一切正常:

      [a, b, c, d, e]
      

      但是如果您提供b 会怎样?当前此页面上的所有实现 返回:

      [b, d, e]
      

      这是不正确,因为c 需要在d 之前完成。解决 这样,我们可以改为将“孩子映射到父母”[1][2]。然后,而不是选择一个 start我们可以选择end

      def tsort(graph, end):
         b = set()
         l = []
         s = [end]
         while s:
            n = s[-1]
            b.add(n)
            for m in graph[n]:
               if not m in b:
                  s.append(m)
            if s[-1] == n:
               s.pop()
               l.append(n)
         return l
      
      graph = {
         'a': [],
         'b': ['a'],
         'c': ['a'],
         'd': ['b', 'c'],
         'e': ['d'],
      }
      
      print(tsort(graph, 'e')) # ['a', 'c', 'b', 'd', 'e']
      
      1. https://rosettacode.org/wiki/Topological_sort
      2. https://github.com/adonovan/gopl.io/blob/master/ch5/toposort/main.go

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-10-26
        • 1970-01-01
        • 2017-02-27
        • 2017-09-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多