在python中看似简单的拓扑排序实现

BPL*_*BPL 19 python algorithm graph-theory depth-first-search topological-sort

这里提取我们得到了一个最小的迭代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'))
Run Code Online (Sandbox Code Playgroud)

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

不要求拓扑排序的版本,这不是对上述例程的微小修改,我已经看过很少.问题不是"如何在python中实现拓扑排序",而是找到上述代码的最小可能调整集成为topological_sort.

附加评论

在原文中,作者说:

不久之前,我读了Guido van Rossen的图表实现,看似简单.现在,我坚持使用复杂性最低的纯python最小系统.我们的想法是能够探索算法.稍后,您可以优化和优化代码,但您可能希望以编译语言执行此操作.

这个问题的目标不是优化iterative_dfs,而是提出一个从它派生的topology_sort的最小版本(只是为了更多地了解图论算法).其实,我想一个更一般的问题可以像给一组最小的算法,{ iterative_dfs,recursive_dfs,iterative_bfs,recursive_dfs},这将是他们的topological_sort推导?虽然这会使问题变得更长/更复杂,但是从iterative_dfs中找出topology_sort就足够了.

Blc*_*ght 23

将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
Run Code Online (Sandbox Code Playgroud)

以下是我修改该代码以进行拓扑排序的方法:

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!
Run Code Online (Sandbox Code Playgroud)

我在这里评论"新东西"的部分是在向上移动时计算出顺序的部分.它检查找到的新节点是否是前一个节点(位于堆栈顶部)的子节点.如果没有,它会弹出堆栈的顶部并将值添加到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
Run Code Online (Sandbox Code Playgroud)

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

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

  • 相同的dict.get调用可用于其他字典查找graph [v]和graph [stack [-1]]。将它们更改为`graph.get(v,[])`和`graph.get(stack [-1],[])`。我不确定是否同时需要两者,但我认为是这样(因为我对这段代码的考虑太多了,已经有一段时间了)。 (2认同)
  • @Saurabh 是的,确实如此。原始问题代码中的“q”变量也是一个堆栈,尽管效率低得多,因为它从头开始推送和弹出,而不是从列表的末尾进行有效操作。这就是 DFS 之所以成为 DFS 的原因。常规(后进先出)队列将提供广度优先遍历。不过,对于我们正在讨论的代码来说,FIFO 队列并不是值得注意的地方。拓扑排序代码中名为“stack”的堆栈替换了递归版本的调用堆栈。 (2认同)

Shi*_*han 8

我的想法基于两个主要观察结果:

  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
Run Code Online (Sandbox Code Playgroud)

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

如果找不到符合条件的孩子(if not children),我们访问了它下面的整个子树.所以它已经准备好被推入ans.这就是我们真正流行的时候.

(不言而喻,它在性能方面并不是很好.不是在children变量中生成所有未访问的子项,而是生成第一个,生成器样式,可能使用过滤器.我们也应该反转它ans = [v] + ans并在最后调用reverseon ans.但是OP坚持简单,省略了这些事情.)

  • 这是一个很好的方法!在概念上非常接近“纯”dfs! (2认同)