【问题标题】:Understanding backtracking (maze algorithm)了解回溯(迷宫算法)
【发布时间】:2014-05-17 14:57:14
【问题描述】:

我试图通过创建一种从迷宫中找到出路的算法来理解递归回溯。所以这是我的“回溯”逻辑:

1) 从当前位置确定您之前未去过的所有开放位置:上、下。 (左右可能被一堵墙挡住了,也可能以前去过)

2) 如果amtOfOpenLocations >= 1 随机选择一个openLocation 并移动到那里。

3) (递归调用)如果没有出错,重复 #1 直到到达路径输出或基本情况。

4) 如果amtOfMoveLocations < 1 后退一步,直到其中一个移动有一个可用的移动位置,不同于已经进行的任何移动。重复步骤 1-3。

所以我的第一个问题是:这是回溯算法的正确实现吗?

如果我的逻辑是正确的,这是我的下一个问题:

所以基本上我所做的就是不断检查已经制作好的位置以外的位置,直到找到出路。当我找到出路时,我会忽略所有其他可能的动作并返回解决方案。例如,如果我在位置 (4,4) 并且我的位置 (4,3), (4,5), (5,4), (3,4) 都可以作为 openLocations 使用,我是对这些位置中的每一个位置进行 4 次递归调用,还是只对其中任何一个位置进行一次递归调用并且逐个测试每个位置?

我的书说“如果您不在出口点,请进行 4 次递归调用以检查所有 4 个方向,并提供 4 个相邻单元格的新坐标。”

* 上关于类似问题的answer 说:“每次迭代多次移动......是错误的” 因此,我很困惑。我的书是错的还是什么?

最后,防止我之前已经检查过的位置的最佳方法是什么?我的想法是保留一个临时数组,其中所有访问过的位置都标有“!”而我的getMoveLocations() 方法将避免任何带有“!”的单元格。有没有更好的方法或者我的方法可以接受?

【问题讨论】:

    标签: java algorithm recursion backtracking maze


    【解决方案1】:

    这是回溯算法的正确实现吗?

    是的,看起来不错。

    不过,沿随机方向移动可能会增加代码的复杂性。不多,如果做得对,但仍然比确定性地向上、向下、向左、然后向右移动要复杂一些。

    另外 - 如果您只有 4 个方向,则将第 1 步作为一个单独的步骤可能是多余的 - 只是循环遍历所有 4 个方向(确定性地,或者通过将它们全部添加到列表中并随机选择一个并将其删除,直到列表空),然后在递归调用之前进行访问/阻塞检查应该更简单。

    我是对这些位置中的每一个进行 4 次递归调用,还是只是对其中任何一个位置进行一次递归调用并逐个测试每个位置?

    我不确定您所说的第二部分是什么意思。 (在 Java 中)连续出现在代码中的递归调用被连续执行。您将对它们中的每一个进行递归调用(您将如何通过 1 个调用访问 4 个位置?)。

    * 上关于类似问题的回答说:“每次迭代多次移动......是错误的”

    这听起来像是被误导的用户的胡言乱语。

    不过,基本思想有一些意义 - 在迭代版本中,您在一次迭代中将许多动作排入队列,但每次迭代只执行一个(pop)。使用递归版本,它就不那么明显了。

    看看Wikipedia 上的深度优先搜索伪代码,它清楚地进行了多次递归调用/每次迭代将多次移动排队:

    Recursive:
    1  procedure DFS(G,v):
    2      label v as discovered
    3      for all edges from v to w in G.adjacentEdges(v) do
    4          if vertex w is not labeled as discovered then
    5              recursively call DFS(G,w)
    
    Iterative:
    1  procedure DFS-iterative(G,v):
    2      let S be a stack
    3      S.push(v)
    4      while S is not empty
    5            v ← S.pop() 
    6            if v is not labeled as discovered:
    7                label v as discovered
    8                for all edges from v to w in G.adjacentEdges(v) do
    9                    S.push(w)
    

    防止我之前检查过的位置的最佳方法是什么?

    您的方法还不错,但更自然的方法是使用 boolean 的数组,尽管上次我检查时,这并不是特别有效的内存。内存效率最高的是BitSet,尽管其代码会稍微复杂一些。

    【讨论】:

    • 对于 4 个递归调用:假设我有 4 个可用位置可以移动到:北、南、东、西。我应该明确地对这些方向中的每一个进行递归调用,还是应该随机选择一个并让步骤 1-3 专门针对那个方向进行?基本上我应该从一个可能的方向开始检查解决方案,还是让我的方法递归地分别检查所有 4 个可能的方向并将问题分割成更小的部分?
    • @SimionMita 随机或确定性移动都可以。正如我的回答中提到的,随机移动可能会使您的代码更复杂(没有太多好处),所以除非您被要求,否则我不会这样做。如果您只有 4 个方向,则第 1 步可能会过大 - 在递归调用之前进行访问检查会简单得多。我不太明白你的最后一句话。
    【解决方案2】:

    我会尝试遍历所有点:

    1. 您的算法大纲看起来正确。第一步可确保您在仅尝试未访问位置时不会遇到无限回归。只要您有可以尝试的位置,您就可以继续前进。在步骤号。 3 递归下降发生。步骤号4 是递归函数的基本情况。这个应该首先测试一下,如果是真的,你必须立即退出函数。因此,它“一旦确定它不可能完成一个有效的解决方案,就会放弃每个部分候选者(“回溯”)。 (来自Wikipedia)。
      通过随机选择一个新的 openLocation,您还可以使其不确定,但仍然是确定的。这是一种随机的。

    2. 您是对每个位置进行 4 次递归调用,还是对其中任何一个位置进行一次递归调用并逐个测试每个位置?好吧......你做后者,在你的组合“树”中一个接一个地测试每个位置。回溯基本上走一棵可能的组合树并向下找到解决方案,如果卡住了,它会向上走。让我们将该树的水平部分称为图层。
      你的书实现它与你做的略有不同。虽然您有一个可以访问的位置列表,但您的书的作者似乎没有。他们将尝试一个位置的所有四个直接邻居,如果它是一堵墙,“出了点问题”(第 3 步),然后他们回溯。在那种情况下,他们不需要一个列表,只需要四个递归调用,一个接一个。同时,您从可能位置的 list 中选择一个随机项目,这些位置是预先为当前单元格计算的。请确保每次从递归下降返回时都不会重新计算该列表。为了使这些列表永久化并仅更新它们,它们不应在递归函数中,而是在您开始回溯之前预先计算,然后在每一步更新它们。

    3. 要么尝试不使用该列表,如本书的作者,要么使用数组,这是正确的方法。我建议使用堆栈数据结构,这样您就可以简单地弹出一个新位置,直到它为空。

    【讨论】:

    • 谢谢,但是在我开始这个项目之前我可能必须熟悉“堆栈数据结构”,因为这种方法看起来和听起来更可取。
    • 当然,我想你会成功的。堆栈数据结构不是大问题。想象一个有两个操作的列表:push 和 pop。它遵循规则:谁先入,谁后出。 :-)