【问题标题】:Two implementation methods of BFS for finding the shortest path, which one is the obvious winner?BFS寻找最短路径的两种实现方法,哪一种是明显的赢家?
【发布时间】:2021-11-26 23:38:30
【问题描述】:

有两种实现 BFS 的方法来找到两个节点之间的最短路径。第一种是使用列表列表来表示路径队列。另一种是维护每个节点到其父节点的映射,在检查相邻节点时,记录其父节点,最后根据父映射进行回溯,找到路径。 (有关详细信息,请参阅此帖子。https://stackoverflow.com/a/8922151/13886907。感谢乔对该问题的回答和代码!)

复制到这里: 第一种方式:

def bfs(graph, start, end):
    # maintain a queue of paths
    queue = []
    # push the first path into the queue
    queue.append([start])
    while queue:
        # get the first path from the queue
        path = queue.pop(0)
        # get the last node from the path
        node = path[-1]
        # path found
        if node == end:
            return path
        # enumerate all adjacent nodes, construct a 
        # new path and push it into the queue
        for adjacent in graph.get(node, []):
            new_path = list(path)
            new_path.append(adjacent)
            queue.append(new_path)

print(bfs(graph, 'A', 'F'))

第二种方式:

def backtrace(parent, start, end):
    path = [end]
    while path[-1] != start:
        path.append(parent[path[-1]])
    path.reverse()
    return path
        

def bfs(graph, start, end):
    parent = {}
    queue = []
    queue.append(start)
    while queue:
        node = queue.pop(0)
        if node == end:
            return backtrace(parent, start, end)
        for adjacent in graph.get(node, []):
            if node not in queue :
                parent[adjacent] = node # <<<<< record its parent 
                queue.append(adjacent)

print(bfs(graph, 'A', 'F'))

和图(有向图)

graph = {'A': ['C', 'D', 'B'],
        'B': ['C', 'E'],
        'C': ['E'],
        'D': ['F'],
        'E': ['F']}

我们可以看到第二种方式可以节省内存成本,因为队列不需要存储路径,并且队列和父映射的空间复杂度都是O(V),其中V是顶点数.而且,最终的回溯过程最多花费额外的 O(V) 时间。

那么,在找到有向图中两个节点之间的最短或所有路径方面,第二种方法是否在所有方面都优于第一种方法?我们可以认为第二种是对 BFS 基础版本的优化(第一种方式)吗?

【问题讨论】:

  • 顺便说一句,queue = []list.pop(0) 极大地减慢了代码速度,使两种算法都成为二次方。如果您需要从序列的前面弹出,尤其是在循环中,请使用 collections.deque()

标签: python algorithm time-complexity breadth-first-search space-complexity


【解决方案1】:

第二个版本更好。这是因为内存分配也需要时间。即这一行:

new_path = list(path)

...就path 的长度而言,时间复杂度为 O(k)。即使在 best 的情况下,图实际上只是从源节点到目标节点的单一路径,第一个代码将花费 O(1) + O(2) + O(3) + .. . + O(n) 执行此 list(path) 调用,即 O(n²)。在这种“快乐路径”的情况下,第二个版本将是 O(n)。当图中的分支因子变得更大时,情况只会变得更糟。

备注你的代码

两个代码sn-ps都有问题:

  • 第一个版本没有防止循环运行的保护。你应该添加一个visited-marker,这样同一个节点就不会被访问两次

  • 第二个版本似乎有这样的保护,但还不够好。它检查下一个节点是否已经在队列中。但是,即使它不是,它也可能以前存在过,而且在这种情况下,也不应该重新审视它。我们可以使用parent来知道该节点是否已经被访问过。

所以这里是修正后的 sn-ps:

def bfs_1(graph, start, end):
    queue = []
    visited = set()
    queue.append([start])
    visited.add(start)
    while queue:
        path = queue.pop(0)
        node = path[-1]
        if node == end:
            return path
        for adjacent in graph.get(node, []):
            if adjacent not in visited:
                visited.add(adjacent)
                new_path = list(path)
                new_path.append(adjacent)
                queue.append(new_path)

def backtrace(parent, start, end):
    path = [end]
    while path[-1] != start:
        path.append(parent[path[-1]])
    path.reverse()
    return path
        

def bfs_2(graph, start, end):
    parent = {}
    queue = []
    queue.append(start)
    parent[start] = None
    while queue:
        node = queue.pop(0)
        if node == end:
            return backtrace(parent, start, end)
        for adjacent in graph.get(node, []):
            if adjacent not in parent:
                parent[adjacent] = node # <<<<< record its parent 
                queue.append(adjacent)

比较

我使用以下测试代码来测试上述算法:

import random
from timeit import timeit

def create_graph(size):
    graph = {}
    nodes = list(range(size))
    for i in nodes:
        graph[i] = set(random.choices(nodes, k=3))
        if i in graph[i]:
            graph[i].remove(i)
        graph[i] = list(graph[i])
    return graph


graph = create_graph(40000)
print("version 1")
print(bfs_1(graph, 1, 2))
print("time used", timeit(lambda: bfs_1(graph, 1, 2), number=10))
print()
print("version 2")
print(bfs_2(graph, 1, 2))
print("time used", timeit(lambda: bfs_2(graph, 1, 2), number=10))

看到它在repl.it上运行

生成的图有 100 000 个节点,分支因子约为 2。边是随机的。大多数情况下,第二种算法比第一种算法快。当解决方案路径较长时,这种差异会变得更加明显。

【讨论】:

    猜你喜欢
    • 2018-09-18
    • 2013-12-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-23
    • 2016-07-30
    相关资源
    最近更新 更多