【问题标题】:BFS, Iterative DFS, and Recursive DFS: When to Mark Node as VisitedBFS、迭代 DFS 和递归 DFS:何时将节点标记为已访问
【发布时间】:2022-01-25 03:37:49
【问题描述】:

在谷歌上搜索了好几个小时后,我仍然没有找到对这个问题的深入、直观和可靠证明的处理方法。我在一些不知名的论坛上找到的最接近的文章是:https://11011110.github.io/blog/2013/12/17/stack-based-graph-traversal.html。我也看到过这个 Stack Overflow 问题DFS vs BFS .2 differences,但回复并没有达成明确的共识。

那么问题来了: 我已经看到它(在维基百科以及 Tim Roughgarden 阐明的算法中)指出,要将 BFS 实现转换为迭代 DFS 实现,需要进行以下两个更改:

非递归实现类似于广度优先搜索,但在两个方面有所不同: 它使用堆栈而不是队列,并且 它延迟检查是否已发现顶点,直到顶点从堆栈中弹出,而不是在添加顶点之前进行此检查。

任何人都可以通过直觉或示例帮助解释这里第二个区别的原因吗?具体来说:BFS、迭代 DFS 和递归 DFS 之间的区别因素是什么,需要将检查推迟到仅针对迭代 DFS 出栈之后?

这是 BFS 的基本实现:

    def bfs(adjacency_list, source):
        explored = [False] * len(adjacency_list)
        queue = deque()
        queue.append(source)
        explored[source] = True
        while queue:
            node = queue.popleft()
            print(node)
            for n in adjacency_list[node]:
                if explored[n] == False:
                    explored[n] = True
                    queue.append(n)

如果我们简单地将队列换成堆栈,我们会得到这个 DFS 的实现:

    def dfs_stack_only(adjacency_list, source):
        explored = [False] * len(adjacency_list)
        stack = deque()
        stack.append(source)
        explored[source] = True
        while stack:
            node = stack.pop()
            print(node)
            for n in adjacency_list[node]:
                if explored[n] == False:
                    explored[n] = True
                    stack.append(n)

这两种算法之间的唯一区别是我们将 BFS 中的队列交换为 DFS 中的堆栈。 DFS 的这种实现实际上产生了不正确的遍历(在一个非简单的图中;可能对于一个非常简单的图它可能无论如何都会产生一个正确的遍历)。

我相信这是上面链接的文章中提到的“错误”。

但是,可以通过以下两种方式之一来解决此问题。

这两种实现中的任何一种都会产生正确的遍历:

首先,上述来源中建议的实现,检查延迟到从堆栈中弹出节点之后。此实现会导致堆栈上出现许多重复项。

    def dfs_iterative_correct(adjacency_list, source):
        explored = [False] * len(adjacency_list)
        stack = deque()
        stack.append(source)
        while stack:
            node = stack.pop()
            if explored[node] == False:
                explored[node] = True
                print(node)
                for n in adjacency_list[node]:
                    stack.append(n)

另外,这是一种流行的在线实现(这个取自 Geeks for Geeks),它也产生了正确的遍历。堆栈上有一些重复项,但几乎没有以前的实现那么多。

def dfs_geeks_for_geeks(adjacency_list, source):
    explored = [False] * len(adjacency_list)
    stack = deque()
    stack.append(source)

    while len(stack):
        node = stack.pop()
        if not explored[node]:
            explored[node] = True
            print(node)

        for n in adjacency_list[node]:
            if not explored[n]:
                stack.append(n)

因此,总而言之,似乎差异不仅在于您何时检查节点的已访问状态,还在于您何时将其实际标记为已访问。此外,为什么立即将其标记为已访问对 BFS 工作得很好,但对 DFS 却不行?非常感谢任何见解!

谢谢!

【问题讨论】:

    标签: graph stack depth-first-search breadth-first-search graph-traversal


    【解决方案1】:

    我认为 BFS 和 DFS 在这方面没有区别。
    我看到“将节点标记为已访问”的两个要求:

    1. 它应该阻止将相邻节点推入堆栈或队列。
    2. 它应该防止再次将节点推入堆栈或队列。

    这些要求适用于 DFS 和 BFS,因此两者的 squance 可以是:

    • 从堆栈或队列中获取节点
    • 将节点标记为已访问
    • 获取节点的邻居
    • 将任何未访问的邻居放入堆栈或队列中

    【讨论】:

    • 感谢您的回复。提到的消息来源指定应该更改顺序。你是说不一定是这样吗?
    • 是的。这就是我的意思。您可以通过仅更改从堆栈到队列的拉取将迭代 DFS 更改为 BFS(或返回)。
    • 谢谢。所以我猜想区别不在于我们何时真正检查一个节点是否已被访问,而更多地在于我们何时将其标记为已访问。我正在编辑问题以提供更多代码 - 你能看一下吗?
    猜你喜欢
    • 1970-01-01
    • 2015-02-27
    • 2015-01-17
    • 2011-12-10
    • 2020-03-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多