我正试图找到有向无环图中最长的路径。目前,我的代码似乎运行时间复杂度为 O(n 3 )。
图表是输入{0: [1,2], 1: [2,3], 3: [4,5] }
#Input: dictionary: graph, int: start, list: path
#Output: List: the longest path in the graph (Recurrance)
# This is a modification of a depth first search
def find_longest_path(graph, start, path=[]):
path = path + [start]
paths = path
for node in graph[start]:
if node not in path:
newpaths = find_longest_path(graph, node, path)
#Only take the new path if its length is greater than the current path
if(len(newpaths) > len(paths)):
paths = newpaths
return paths
它返回表单中的节点列表,例如[0,1,3,5]
如何使这比 O(n 3 )更有效?递归是解决这个问题的正确方法,还是我应该使用不同的循环?
答案 0 :(得分:3)
您可以在 O(n + e)中解决此问题(即节点数+边缘线性)。
这个想法是你首先创建一个拓扑排序(我是Tarjan's algorithm的粉丝)和一组反向边。如果您可以分解问题以利用现有解决方案,那将始终有所帮助。
然后向后走拓扑排序,推动每个父节点与子节点的距离+ 1(如果有多条路径则保持最大值)。跟踪目前距离最远的节点。
当您完成对具有距离的所有节点的注释时,您可以从具有最长距离的节点开始,该节点将是您最长的路径根,然后沿着图表向下选择正好比一个小于当前节点(因为它们位于关键路径上)。
通常,在尝试寻找最佳复杂度算法时,不要害怕一个接一个地运行多个阶段。顺序运行的五个 O(n)算法仍然 O(n)并且仍然优于 O(n 2 )强大>从复杂的角度来看(尽管实际运行时间可能更差,取决于不变的成本/因素和 n 的大小)。
ETA:我刚注意到你有一个开始节点。这使得它只是进行深度优先搜索并保持到目前为止看到的最长解决方案的情况,无论如何只是 O(n + e)。递归很好,或者你可以保留一个列表/堆栈的访问节点(每次回溯时你都要小心找到下一个孩子)。
当您从深度优先搜索回溯时,您需要存储从该节点到叶子的最长路径,这样您就不会重新处理任何子树。这也将作为visited
标志(即除了执行node not in path
测试之外还在递归之前进行node not in subpath_cache
测试)。您可以存储长度,然后根据上面讨论的顺序值完成重建路径(关键路径),而不是存储子路径。
ETA2 :这是一个解决方案。
def find_longest_path_rec(graph, parent, cache):
maxlen = 0
for node in graph[parent]:
if node in cache:
pass
elif node not in graph:
cache[node] = 1
else:
cache[node] = find_longest_path_rec(graph, node, cache)
maxlen = max(maxlen, cache[node])
return maxlen + 1
def find_longest_path(graph, start):
cache = {}
maxlen = find_longest_path_rec(graph, start, cache)
path = [start]
for i in range(maxlen-1, 0, -1):
for node in graph[path[-1]]:
if cache[node] == i:
path.append(node)
break
else:
assert(0)
return path
请注意,我已删除了node not in path
测试,因为我假设您实际上正在提供所声明的DAG。如果你想要那个检查,你应该提出错误而不是忽略它。另请注意,我已将断言添加到else
的{{1}}子句中,以记录我们必须始终在路径中找到有效的下一个(顺序)节点。
ETA3:最后的for
循环有点令人困惑。我们正在考虑的是,在关键路径中,所有节点距离必须是连续的。考虑节点0是距离4,节点1是距离3而节点2是距离1.如果我们的路径开始for
我们有一个矛盾,因为节点0离叶子不比2更远。
答案 1 :(得分:1)
我建议使用一些非算法改进(这些改进与Python代码质量有关):
def find_longest_path_from(graph, start, path=None):
"""
Returns the longest path in the graph from a given start node
"""
if path is None:
path = []
path = path + [start]
max_path = path
nodes = graph.get(start, [])
for node in nodes:
if node not in path:
candidate_path = find_longest_path_from(graph, node, path)
if len(candidate_path) > len(max_path):
max_path = candidate_path
return max_path
def find_longest_path(graph):
"""
Returns the longest path in a graph
"""
max_path = []
for node in graph:
candidate_path = find_longest_path_from(graph, node)
if len(candidate_path) > len(max_path):
max_path = candidate_path
return max_path
更改说明:
def find_longest_path_from(graph, start, path=None):
if path is None:
path = []
我已将find_longest_path
重命名为find_longest_path_from
,以便更好地解释它的作用。
将path
参数更改为默认参数值为None
而不是[]
。除非您知道您将从中受益,否则您希望避免将可变对象用作Python中的默认参数。这意味着您通常应默认将path
设置为None
,然后在调用该函数时,检查是否path is None
并相应地创建一个空列表。
max_path = path
...
candidate_path = find_longest_path_from(graph, node, path)
...
我已将您的变量名称从paths
更新为max_path
和newpaths
更新为candidate_path
。这些是令人困惑的变量名称,因为它们引用了复数路径 - 暗示它们存储的值由多条路径组成 - 实际上它们每条只保持一条路径。我试图给他们更多的描述性名称。
nodes = graph.get(start, [])
for node in nodes:
您的示例输入中出现代码错误,因为图表的叶节点不是dict
中的键,因此当graph[start]
为{KeyError
时,start
会引发2
例如{1}}。这通过返回空列表来处理start
不是graph
中的键的情况。
def find_longest_path(graph):
"""
Returns the longest path in a graph
"""
max_path = []
for node in graph:
candidate_path = find_longest_path_from(graph, node)
if len(candidate_path) > len(max_path):
max_path = candidate_path
return max_path
一种在图中找到迭代键的最长路径的方法。这与find_longest_path_from
的算法分析完全不同,但我想将其包括在内。