【问题标题】:Can someone describe through code a practical example of backtracking with iteration instead of recursion?有人可以通过代码描述一个使用迭代而不是递归进行回溯的实际示例吗?
【发布时间】:2012-11-26 18:31:39
【问题描述】:

递归使回溯变得容易,因为它保证您不会再走同样的路。所以你的路径的所有分支只被访问一次。我正在尝试将回溯尾递归(带累加器)算法转换为迭代。我听说将完美尾-递归算法转换为迭代应该很容易。但我被困在回溯部分。

任何人都可以通过代码提供一个示例,以便我和其他人可以想象回溯是如何完成的吗? 我认为这里不需要堆栈,因为我有一个使用累加器的完美尾递归算法,但在这里我可能错了。

【问题讨论】:

  • 来自wikipediaIn computer science, a tail call is a subroutine call that happens inside another procedure as its final action。据我所知,具有多个递归调用的函数 - 根据定义不是尾递归,并且由于回溯算法确实有不止一个调用,它们不是“尾递归”。
  • @amit:只要它们都是最后一个动作,函数执行(递归)调用多少个地方都没有关系。 (“最后一个动作”的含义完全取决于语言,但通常调用的形式必须是“return func(args...);”

标签: algorithm data-structures recursion tail-recursion


【解决方案1】:

如果函数实际上是递归的,那么转换如下(这是理解TCO的编译器会为你做的,所以你不应该自己做):

原文:

function func(a1, a2, a3...)
   ... doesn't contain either return or call
   return val
   ...
   return func(x1, x2, x3...)
   ...
   ... etc.

转换为:

function func(a1, a2, a3...)
   func:  // label for goto (yuk!)
     ...
     return val  // No change
     ...
     a1 = x1; a2 = x2; a3 = x3...; goto func;
     ...
     ... etc.

为了使这种转换与相互协同递归的函数一起工作,您需要将它们组合成一个函数,每个函数都带有一个标签。如上所述,简单的return 语句不会被更改,return foo(...) 会变成对参数变量的赋值,然后是goto foo

当然,在组合函数时,您可能需要重命名局部变量以避免冲突。而且你也将失去使用多个顶级函数的能力,除非你在顶部入口点添加类似switch 语句(带有gotos),在任何标签之前。 (事实上​​,在允许goto case foo 的语言中,您可以只使用大小写标签作为标签。)

goto 的使用当然是丑陋的。如果您使用的语言最好保证尾调用优化,或者失败,至少做出合理的尝试并在失败时报告,那么绝对没有动机替换递归解决方案,(在我看来)几乎总是更具可读性。

在某些情况下,可以将 goto 和标签替换为 while (1) { ... }or other such loops, but that involves replacing thegotos withcontinue`(或等效项)之类的东西,如果它们不起作用,那将不起作用'嵌套在其他循环中。因此,您实际上可能会浪费大量时间来使丑陋的转换稍微不那么丑陋,但最终仍然无法获得像原始程序那样可读的程序。

我现在停止宣传递归。 :)

已编辑(我忍不住,抱歉)

这是 Lua 中的一个回溯 n-queens 解决方案(确实有 TCO),由一个尾递归求解器和一个尾递归验证器组成:

function solve(legal, n, first, ...)
  if first == nil                    -- Failure
    then return nil
  elseif first >= n                  -- Back-track
    then return solve(legal, n, ...)
  elseif not legal(first + 1, ...)   -- Continue search
    then return solve(legal, n, first + 1, ...)
  elseif n == 1 + select("#", ...)   -- Success
    then return first + 1, ...
  else                               -- Forward
    return solve(legal, n, 0, first + 1, ...)
  end
end

function queens_helper(dist, first, second, ...)
  if second == nil
    then return true
  elseif first == second or first - dist == second or first + dist == second
    then return false
  else
    return queens_helper(dist + 1, first, ...)
  end
end

function queens_legal(...) return queens_helper(1, ...) end

-- in case you want to try n-rooks, although the solution is trivial.    
function rooks_legal(first, second, ...)
  if second == nil then return true
  elseif first == second then return false
  else return rooks_legal(first, ...)
  end
end

function queens(n) return solve(queens_legal, n, 0) end

【讨论】:

  • 除非P=NP,否则不可能将一般情况递归(尤其是“多次调用”,(每次调用中的多次调用))转换为循环+常量空间。我将其详细说明为answer to the OPs previous question(请注意,如果 P=NP,这与不可能的事实并不矛盾,但如果 P!=NP,则肯定是)
  • @amit,没错,但是这样的递归不能是纯尾递归,因为在尾递归中,每次调用只有一次调用。可能有多个调用点,但只能执行一个。由于 OP 说尾递归,我认为“一般情况递归”问题不相关。
  • 我想我无法理解(非平凡的)回溯算法如何也可以是尾递归的。我的直觉告诉我没有,所以任何答案基本上都是空洞的:|
  • @amit:在累积样式中,您传递一个累加器并在达到结束条件时返回它。为了使这个与回溯一起工作,你需要传递一个累加器列表;失败时,你进行尾递归,删除列表的头部。在回溯点,您使用推到列表头部的新累加器进行尾递归。
  • 这基本上相当于一个堆栈+循环,据我了解,这不是OP所追求的:|
猜你喜欢
  • 2018-12-29
  • 1970-01-01
  • 2012-11-26
  • 1970-01-01
  • 2023-03-09
  • 1970-01-01
  • 2021-12-24
  • 1970-01-01
  • 2023-03-31
相关资源
最近更新 更多