从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
,而是提出一个源自它的topology_sort的最小版本(仅仅是为了更多地了解图论算法)。事实上,我想一个更普遍的问题可能是给定一组最小算法,{iterative_dfs
,recursive_dfs
,iterative_bfs
,recursive_dfs
},他们会是什么topological_sort派生?虽然这会使问题变得更长/更复杂,但是从iterative_dfs中找出topology_sort就足够了。
答案 0 :(得分:18)
将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
字典中的值是集合而不是列表,则效率会更高。图形通常不关心其边缘的保存顺序,因此进行此类更改不应导致大多数其他算法出现问题,尽管生成或更新图形的代码可能需要修复。如果您打算扩展图形代码以支持加权图形,那么您最终可能会将列表更改为从节点到权重的字典映射,这对于此代码也是如此(字典查找是{{1}就像设置查找一样)。或者,如果O(1)
无法直接修改,我们可以自己构建我们需要的集合。
作为参考,这里是DFS的递归版本,并对其进行修改以进行拓扑排序。所需的修改确实非常小:
graph
就是这样!一行被删除,类似的一行被添加到不同的位置。如果你关心性能,你应该在第二个辅助函数中执行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
函数中执行return result[::-1]
。但使用recursive_topological_sort
是一个更小的变化。
还值得注意的是,如果您想要整个图的拓扑顺序,则不需要指定起始节点。实际上,可能没有一个节点可以让您遍历整个图形,因此您可能需要进行多次遍历以获取所有内容。在迭代拓扑排序中实现这种情况的简单方法是将insert(0, ...)
初始化为q
(所有图形键的列表),而不是仅包含单个起始节点的列表。对于递归版本,将list(graph)
的调用替换为在图中的每个节点上调用辅助函数的循环,如果它尚未在recursive_helper(node)
中。
答案 1 :(得分:5)
我的想法基于两个主要观察结果:
这两个都帮助我们完全像递归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
。 '访问'而不是' pop',因为我们需要能够再次返回此节点。我们找到了当前节点的所有未访问过的子节点。并且只推送第一个堆叠(q.append(children[0])
),而不是全部堆叠在一起。同样,这正是我们对递归dfs所做的。
如果找不到符合条件的孩子(if not children
),我们已访问了其下的整个子树。因此,它已准备好被推入ans
。这就是我们真正流行的时候。
(不言而喻,它在性能方面并不是很好。我们应该生成第一个,生成器样式,可能使用过滤器,而不是在children
变量中生成所有未访问的子项。我们应该同样反转ans = [v] + ans
并在最后调用reverse
上的ans
。但这些内容因OP对简单性的坚持而被忽略。)
答案 2 :(得分:0)
我也在尝试简化此过程,所以我想到了:
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年。
答案 3 :(得分:0)
给定您的示例图:
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']