在有向图中使用 DFS 进行循环检测是不是绝对需要回溯?

Posted

技术标签:

【中文标题】在有向图中使用 DFS 进行循环检测是不是绝对需要回溯?【英文标题】:Is backtracking absolutely necessary for cycle detection using DFS in directed graph?在有向图中使用 DFS 进行循环检测是否绝对需要回溯? 【发布时间】:2013-12-12 22:10:42 【问题描述】:

我遇到了这个SO post,其中建议在有向图中使用 DFS 进行循环检测由于回溯而更快。我在这里引用该链接:

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

此外,如果您的图表是有向的,那么您不仅要记住 如果你访问过一个节点,还有你是如何到达那里的。 否则你可能认为你找到了一个循环,但实际上 你所拥有的只是两条独立的路径 A->B 但这并不意味着 有一条路径 B->A。通过深度优先搜索,您可以标记节点 在您下降时访问并在您回溯时取消标记。

为什么回溯很重要?

有人可以用示例图解释给定A->B 示例中的含义吗?

最后,我有一个DFS 代码用于有向图中的循环检测,它不使用回溯,但仍然在O(E+V) 中检测循环。

public boolean isCyclicDirected(Vertex v)
  if (v.isVisited) 
    return true;
  
  v.setVisited(true);
  Iterator<Edge> e = v.adj.iterator();
  while (e.hasNext()) 
    Vertex t = e.next().target;
    // quick test:
    if (t.isVisited) 
      return true;
    
    // elaborate, recursive test:
    if (isCyclicDirected(t)) 
      return true;
    
  
  // none of our adjacent vertices flag as cyclic
  v.setVisited(false);
  return false;

【问题讨论】:

if (t.isVisited) return true; 是不必要的,因为当 isCyclicDirected(t) 被调用时,它首先会检查传入的 Vertex 是否已被访问。 @Floegipoky:同意!非常有效的观点!但是算法总体上是正确的吧? 不,很遗憾它不正确。但是,如果您在return false; 之前添加了v.setVisited(false);,我想它会是! :) @Floegipoky:是的,我刚刚想通了。谢谢你,我正在编辑帖子 一般来说,您应该避免修复问题中的代码,因为这往往会使某些(部分或全部)答案无效。在这种情况下,它可能已经够小了。 【参考方案1】:

为什么需要回溯:

A -> B
^ \
|  v
D <- C

如果你去A -&gt; B并且你不回溯,你会停在那里,你不会找到循环。

您的算法确实回溯。您只是将它包装在递归中,因此它可能看起来不像您期望的那样。您为其中一个邻居递归,如果找不到循环,则该调用返回并且您尝试其他邻居 - 这是回溯。

为什么你需要记住你是如何到达现在的位置的:

A -> B
  \  ^
   v |
     C

上图没有环,但是如果你去A -&gt; B,然后A -&gt; C -&gt; B,如果你不记得路径,你会认为有。

正如链接帖子中提到的,您可以在返回代码之前将访问标志设置为 false(我看到您现在已经完成了) - 这将起到记住路径的作用。

【讨论】:

1.当我回溯时,我应该 setVisited 标志为假,在我进行下一次迭代之前,我从上面引用 With a depth first search you can mark nodes as visited as you descend and unmark them as you backtrack.,但我没有取消设置标志 visited。那你为什么说我回溯 它不会止步于此 (A-&gt;B)。看看我的代码这是可行的,因为我遍历了A 的所有相邻节点,即在我通过A-&gt;B 之后,我回来做A-&gt;C 和任何其他相邻节点..所以我认为回溯不是必需的 对不起,我现在只是在混淆自己。你回溯,因为这就是递归所做的——你为其中一个邻居递归,如果这没有找到一个循环,那么调用返回并且你尝试另一个邻居——这就是回溯。没错,它不会停在A -&gt; B,但是根据我的回答的第二部分,对于一些没有循环的图,您的代码将返回 true。 是的,您提供的示例图失败了,但我修复了关闭 visited 标志,现在它工作正常。因为我在做setVisited(false),所以我在做回溯,我记得回溯模式为set, recurse, unset 在类似的注释上,我看到提供了带有回溯解决方案的 dfs 以在此处的图中查找所有循环:***.com/questions/546655/finding-all-cycles-in-graph 第三个答案,但我看不到它是如何找到所有循环的,我把评论留在那里。但没有得到回复【参考方案2】:

值得指出的是,这种标记算法是对链表循环检测的朴素方法的改编,该方法涉及跟踪迄今为止访问的每个节点。在这种情况下,递归所遵循的路径被视为链表并应用链表算法。空间复杂度使算法对链表来说不是最优的,因为你需要持有对链表中每个节点的引用,它是 O(n)。但是,当您将其应用于平衡良好的图形时,空间复杂度会降至 O(logn)。在图是一棵树的情况下,空间复杂度会降低到 O(n),但您会得到 O(n) 时间复杂度。

另外,算法仍然不正确。给定一个带有节点 AB 以及一条边 B-&gt;B 的图,isCyclicDirected(A) 永远不会检测到循环。

【讨论】:

请注意,如果仅针对一个顶点调用该函数,则由于您提到的原因该算法不正确(如果为每个顶点调用该函数,它将不起作用for a different reason,但是所有访问过的标志也每次调用后都必须重置,这会使运行时间比提到的要长)。 我认为编辑后的版本如果为每个顶点调用它会返回正确的答案,但正如你所说的运行时将是不可接受的 您应该能够通过为主调用中访问的顶点设置另一个访问数组来提高运行时间,您再也不会访问该数组。这应该会显着减少运行时间,但我相信在最坏的情况下它仍然是指数级的。 @Dukeling:我提供的版本已经具有连接图的最佳运行时间,对吗?为什么有另一个访问的顶点数组是有帮助的?我正在标记一个已经通过其字段访问过的数组.. @Floegipoky:请看我上面对 Dukeling 的评论【参考方案3】:

只有当您的图表没有任何情况可以通过两条不同的路径从节点 A 到达节点 B 时,回溯才不是必需的。在上一个答案中提到的情况下,您的算法将检测到误报: A -> B \ ^ v | C 但是,如果您添加回溯,即使在上述情况下,您的中音也将完美运行。

【讨论】:

以上是关于在有向图中使用 DFS 进行循环检测是不是绝对需要回溯?的主要内容,如果未能解决你的问题,请参考以下文章

python 实现有向图和相关的DFS,循环检测算法

如何使用递归 DFS 查找图是不是包含循环?

使用 DFS 算法对有向图和无向图进行拓扑排序

循环有向图和无向图

undirected_dfs 是不是检测到图的所有循环?

有向图欧拉路模板