为啥用 DFS 而不是 BFS 在图中寻找循环

Posted

技术标签:

【中文标题】为啥用 DFS 而不是 BFS 在图中寻找循环【英文标题】:Why DFS and not BFS for finding cycle in graphs为什么用 DFS 而不是 BFS 在图中寻找循环 【发布时间】:2011-02-21 14:46:48 【问题描述】:

DFS 主要用于在图中找到循环,而不是 BFS。有什么原因吗?两者都可以找到一个节点是否已经 在遍历树/图时访问。

【问题讨论】:

在有向图中,只能使用DFS来检测循环;但在无向图中两者都可以使用。 【参考方案1】:

我发现BFS和DFS都可以用来检测循环。 有些问题提到 BFS 不能与有向图一起使用。我谦虚地不同意。

在 BFS-Queue 中,我们可以跟踪父节点列表/集,如果再次遇到相同的节点(直接父节点除外),我们可以将其标记为循环。所以这种方式 BFS 也应该适用于有向图。

虽然这与 DFS 相比内存效率非常低,这也是我们主要使用 DFS 的原因。

    DFS 内存高效 DFS 更易于可视化,因为它已经使用了显式/隐式堆栈

【讨论】:

【参考方案2】:

当您想在有向图中找到包含给定节点的最短循环时,您必须使用BFS

例如:

如果给定节点为 2,则它属于三个循环 - [2,3,4][2,3,4,5,6,7,8,9][2,5,6,7,8,9]。最短的是[2,3,4]

为了使用 BFS 实现这一点,您必须使用适当的数据结构显式维护访问节点的历史记录。

但对于所有其他目的(例如:寻找任何循环路径或检查是否存在循环),DFS 是其他人提到的原因的明确选择。

【讨论】:

【参考方案3】:

我不知道为什么我的提要中出现了这么老的问题,但之前的所有答案都是错误的,所以...

DFS 用于在有向图中查找循环,因为它有效

在 DFS 中,每个顶点都被“访问”,其中访问顶点意味着:

    顶点开始

    访问从该顶点可到达的子图。这包括跟踪从该顶点可到达的所有未跟踪边,并访问所有可到达的未访问顶点。

    顶点完成。

关键特性是在顶点完成之前跟踪从顶点到达的所有边。这是 DFS 的一个特性,但不是 BFS。其实这就是DFS的定义。

因为这个特性,我们知道当一个循环中的第一个顶点开始时:

    没有跟踪循环中的任何边缘。我们知道这一点,因为您只能从循环中的另一个顶点获取它们,而我们正在讨论要开始的第一个顶点。 从该顶点可到达的所有未跟踪边将在完成之前被跟踪,并且包括循环中的所有边,因为它们中没有一条被跟踪。因此,如果有一个循环,我们会在它开始之后,但在它结束之前找到一条回到第一个顶点的边;和 由于跟踪的所有边都可以从每个开始但未完成的顶点到达,因此找到到这样一个顶点的边总是表示一个循环。

所以,如果有一个循环,那么我们保证找到一个开始但未完成的顶点的边(2),如果我们找到这样的边,那么我们保证有一个循环(3 )。

这就是为什么使用 DFS 在有向图中查找循环的原因。

BFS 不提供此类保证,因此它不起作用。 (尽管包含 BFS 或类似子程序的非常好的循环查找算法)

另一方面,当任意一对顶点之间有两条路径时,即当它不是一棵树时,无向图就有一个循环。这在 BFS 或 DFS 期间很容易检测到——追踪到新顶点的边形成一棵树,任何其他边都表示一个循环。

【讨论】:

确实,这是这里最相关(也许是唯一)的答案,详细说明了实际原因。【参考方案4】:

深度优先搜索比广度优先搜索更节省内存,因为您可以更快地回溯。如果您使用调用堆栈,也更容易实现,但这依赖于最长的路径不会溢出堆栈。

此外,如果您的图表是directed,那么您不仅要记住您是否访问过某个节点,还要记住您是如何到达那里的。否则你可能会认为你找到了一个循环,但实际上你只有两条独立的路径 A->B,但这并不意味着有一条路径 B->A。 例如,

如果你从0开始做BFS,它会检测到存在循环但实际上没有循环。

通过深度优先搜索,您可以在下降时将节点标记为已访问,并在回溯时取消标记它们。有关此算法的性能改进,请参阅 cmets。

对于best algorithm for detecting cycles in a directed graph,您可以查看Tarjan's algorithm。

【讨论】:

(内存高效,因为您可以更快地回溯,并且更容易实现,因为您可以让堆栈负责存储打开列表,而不必显式维护它。) IMO,只有依靠尾递归才会更容易。 “在您回溯时取消标记它们” - 后果自负!这很容易导致 O(n^2) 行为,特别是这样的 DFS 会将交叉边误解为“树”边(“树”边也将是用词不当,因为它们实际上不再形成树) @Dimitris Andreo:您可以使用三个访问状态而不是两个来提高性能。对于有向图,“我以前见过这个节点”和“这个节点是循环的一部分”是有区别的。对于无向图,它们是等价的。 没错,你肯定需要第三种状态(使算法线性化),所以你应该考虑修改那部分。【参考方案5】:

要证明一个图是循环的,你只需要证明它有一个循环(边直接或间接指向自身)。

在 DFS 中,我们一次取一个顶点并检查它是否有循环。一旦找到一个循环,我们就可以省略检查其他顶点。

在 BFS 中,我们需要同时跟踪许多顶点边,而且通常在最后你会发现它是否有循环。随着图大小的增长,与 DFS 相比,BFS 需要更多的空间、计算和时间。

【讨论】:

【参考方案6】:

这有点取决于您是在谈论递归还是迭代实现。

递归-DFS 访问每个节点两次。 Iterative-BFS 访问每个节点一次。

如果要检测循环,则需要在添加邻接关系之前和之后调查节点——无论是在节点上“开始”还是在节点上“结束”时。

这需要在 Iterative-BFS 中做更多的工作,因此大多数人选择 Recursive-DFS。

请注意,使用 std::stack 的 Iterative-DFS 的简单实现与 Iterative-BFS 存在相同的问题。在这种情况下,您需要将虚拟元素放入堆栈中以跟踪您何时“完成”对节点的工作。

有关 Iterative-DFS 如何需要额外工作来确定您何时“完成”节点的更多详细信息,请参阅此答案(在 TopoSort 的上下文中回答):

Topological sort using DFS without recursion

希望这能解释为什么人们偏爱 Recursive-DFS 来解决您需要确定何时“完成”处理节点的问题。

【讨论】:

这是完全错误的,因为无论你是使用递归还是通过迭代消除递归都没有关系。您可以实现一个迭代 DFS,它访问每个节点两次,就像您可以实现一个递归变体,它只访问每个节点一次。【参考方案7】:

BFS 不适用于有向图寻找循环。将 A->B 和 A->C->B 视为图中从 A 到 B 的路径。 BFS 会说,在沿着 B 被访问的路径之一走之后。当继续下一条路径时,它会说再次找到标记的节点B,因此,那里有一个循环。显然这里没有循环。

【讨论】:

您能解释一下 DFS 将如何清楚地识别您的示例中不存在该循环吗?我同意提供的示例中不存在该循环。但是如果我们从 A->B 然后 A- >C->B 我们会发现 B 已经被访问过并且它的父节点是 A 而不是 C ..我读到 DFS 将通过比较已经访问元素的父节点与我们正在检查的方向的当前节点来检测循环时刻。我得到 DFS 错误还是什么? 您在这里展示的只是这个特定的实现不起作用,而不是 BFS 不可能。事实上,它可能的,尽管它需要更多的工作和空间。 @Prune:这里的所有线程(我认为)都试图证明 bfs 不能用于检测周期。如果您知道如何反证,您应该提供证明。仅仅说努力更大是不够的 由于在链接的帖子中给出了算法,我觉得这里不适合重复大纲。 我找不到任何链接的帖子,因此要求相同。我同意你关于 bfs 能力的观点,并且刚刚考虑过实施。感谢您的提示:)【参考方案8】:

如果图是无向的,则 BFS 可能是合理的(请作为我的客人展示使用 BFS 的有效算法,该算法将在有向图中报告循环!),其中每个“交叉边缘”定义一个循环。如果交叉边是v1, v2,并且包含这些节点的根(在BFS树中)是r,那么循环是r ~ v1 - v2 ~ r~是一条路径,-是一条单边),这几乎可以像在 DFS 中一样容易地报告。

使用 BFS 的唯一原因是,如果您知道您的(无向)图将具有长路径和小路径覆盖(换句话说,既深又窄)。在这种情况下,BFS 的队列需要的内存比 DFS 的堆栈要少(当然两者仍然是线性的)。

在所有其他情况下,DFS 显然是赢家。它适用于有向图和无向图,并且报告循环很简单 - 只需将任何后边连接到祖先的路径给后代,你就得到了循环。总而言之,在这个问题上,比 BFS 更好更实用。

【讨论】:

【参考方案9】:

如果您将循环放置在树中的随机位置,则 DFS 会在它覆盖大约一半的树时触发循环,一半的时间它已经遍历循环所在的位置,一半的时间它不会(并且平均会在树的其余一半中找到它),因此它将平均评估大约 0.5*0.5 + 0.5*0.75 = 0.625 的树。

如果您将循环放置在树中的随机位置,则 BFS 只会在评估该深度的树层时才会触发循环。因此,您通常最终不得不评估平衡二叉树的叶子,这通常会导致评估更多的树。特别是,有 3/4 的时间,两个链接中的至少一个出现在树的叶子中,在这些情况下,您必须平均评估树的 3/4(如果有一个链接)或 7/树的 8 个(如果有两个),所以你已经达到了搜索 1/2*3/4 + 1/4*7/8 = (7+12)/32 = 21/32 = 的期望0.656... 的树,甚至没有增加搜索树的成本,并且在远离叶子节点的地方添加了一个循环。

另外,DFS 比 BFS 更容易实现。因此,除非您对周期有所了解(例如,周期可能在您搜索的根附近,此时 BFS 会给您带来优势),否则它就是您要使用的工具。

【讨论】:

那里有很多神奇的数字。我不同意“DFS 更快”的论点。这完全取决于输入,在这种情况下,没有输入比另一个输入更常见。 @Vlad - 数字并不神奇。它们是手段,是这样陈述的,并且考虑到我所说的假设,计算起来几乎是微不足道的。如果通过均值近似是一个不好的近似,那将是一个有效的批评。 (我明确指出,如果你可以对结构做出假设,答案可能会改变。) 这些数字很神奇,因为它们没有任何意义。你拿了一个案例 DFS 做得更好,并将这些结果外推到一般案例中。您的陈述是没有根据的:“当 DFS 覆盖了大约一半的树时,它往往会达到循环”:证明这一点。更不用说你不能谈论树中的循环。根据定义,树没有循环。我只是不明白你的意思是什么。 DFS 会走一条路,直到它遇到死胡同,所以你无法知道它平均会探索多少 GRAPH(不是树)。您只是随机选择了一个没有任何证据的案例。 @Vlad - 所有非循环全连接无向图都是(无根无向)树。我的意思是“一个除了一个虚假链接之外的树形图”。也许这不是该算法的主要应用——也许你想在一些有很多链接的纠结图中找到循环,这使得它不是一棵树。但如果它是树状的,在所有图上平均,任何节点都有可能成为所述虚假链接的来源,这使得当链接被命中时,预期的树覆盖率为 50%。所以我接受这个例子可能没有代表性。但数学应该是微不足道的。【参考方案10】:
    DFS 更容易实现 一旦 DFS 找到一个循环,堆栈将包含形成循环的节点。 BFS 并非如此,因此如果您还想打印找到的循环,则需要做额外的工作。这让 DFS 更加方便。

【讨论】:

以上是关于为啥用 DFS 而不是 BFS 在图中寻找循环的主要内容,如果未能解决你的问题,请参考以下文章

LeetCode 1971[并查集 DFS BFS] 寻找图中是否存在路径 HERODING的LeetCode之路

动画演示广度优先算法寻找最短路径

为啥 BFS 用 2 种颜色标记节点,而 DFS 用 3 种颜色标记节点?

八数码问题+路径寻找问题+bfs(隐式图的判重操作)

算法-03 | 深度优先DFS| 广度优先BFS

为啥我在图中的 DFS 循环检测总是返回 true?