是动态编程回溯缓存

时间:2014-04-07 16:41:36

标签: algorithm theory dynamic-programming backtracking

我一直都在想这个。没有书明确说明这一点。

回溯正在探索所有可能性,直到我们发现一种可能性无法引导我们找到可能的解决方案,在这种情况下我们放弃它。

据我所知,动态编程的特点是重叠的子问题。那么,动态编程是否可以表示为缓存回溯(对于以前探索过的路径)?

由于

4 个答案:

答案 0 :(得分:8)

这是动态编程的一个方面,但还有更多内容。

对于一个简单的例子,请使用Fibonacci数字:

F (n) =
        n = 0:  0
        n = 1:  1
        else:   F (n - 2) + F (n - 1)

我们可以调用上面的代码“回溯”或“递归”。 让我们将其转换为“使用缓存回溯”或“使用memoization进行递归”:

F (n) =
       n in Fcache:  Fcache[n]
       n = 0:  0, and cache it as Fcache[0]
       n = 1:  1, and cache it as Fcache[1]
       else:  F (n - 2) + F (n - 1), and cache it as Fcache[n]

不过,还有更多内容。

如果问题可以通过动态编程解决,那么它们之间存在状态和依赖关系的有向非循环图。 有一个国家对我们感兴趣。 还有基本状态,我们立即知道答案。

  • 我们可以从我们感兴趣的顶点遍历那个图形到它的所有依赖关系,从它们到它们依赖的所有依赖关系等等,停止在基本状态进一步分支。 这可以通过递归来完成。

  • 有向无环图可以视为顶点上的偏序。我们可以拓扑排序该图形并按排序顺序访问顶点。 此外,您可以找到一些简单的总订单,这与您的部分订单一致。

另请注意,我们经常可以观察一些状态结构。 例如,状态通常可以表示为整数的整数或元组。 因此,我们可以预先分配一个更容易和更快速使用的常规数组,而不是使用通用缓存技术(例如,关联数组来存储状态 - >值对)。


回到我们的Fibonacci示例,偏序关系就是状态n >= 2取决于状态n - 1n - 2。 基本状态为n = 0n = 1。 与此订单关系一致的简单总订单是自然顺序:012...。 以下是我们的开始:

Preallocate array F with indices 0 to n, inclusive
F[0] = 0
F[1] = 1

很好,我们有访问州的顺序。 现在,什么是“访问”? 还有两种可能性:

(1)“Backward DP”:当我们访问状态u时,我们会查看其所有依赖项v并计算该状态的答案u

for u = 2, 3, ..., n:
    F[u] = F[u - 1] + F[u - 2]

(2)“转发DP”:当我们访问某个州u时,我们会查看所有依赖它的州v,并在每个州u中占v { {1}}:

for u = 1, 2, 3, ..., n - 1:
    add F[u] to F[u + 1]
    add F[u] to F[u + 2]

请注意,在前一种情况下,我们仍然直接使用Fibonacci数的公式。 然而,在后一种情况下,命令性代码不能通过数学公式容易地表达。 尽管如此,在某些问题上,“前向DP”方法更直观(现在没有好的例子;有谁愿意贡献它?)。


动态编程的另一个用途是难以表达为回溯:Dijkstra的算法也可以被认为是DP。 在算法中,我们通过向顶点添加顶点来构造最优路径树。 当我们添加一个顶点时,我们使用的事实是它的整个路径 - 除了路径中的最后一个边缘 - 已经知道是最优的。 因此,我们实际上使用了一个子问题的最优解决方案 - 这正是我们在DP中所做的事情。 但是,我们事先不知道向树中添加顶点的顺序。

答案 1 :(得分:5)

没有。或者更确切地说。

在回溯中,你往下走,然后备份每条路径。但是,动态编程是自下而上的,因此您只能获得备份部分而不是原始的下降部分。此外,动态编程的顺序首先是宽度,而回溯通常是深度优先。

另一方面,memoization(动态编程非常接近的表兄弟)经常用作缓存的回溯,正如你所描述的那样。

答案 2 :(得分:3)

是和否。

动态编程基本上是一种实现递归公式的有效方法,而自上而下的DP实际上是使用递归+缓存多次完成的:

def f(x):
  if x is in cache:
    return cache[x]
  else:
    res <- .. do something with f(x-k)
    cahce[x] <- res
    return res

请注意,自下而上的DP实现完全不同 - 但仍然非常遵循递归方法的基本原则,并且在每个步骤中计算&#39;关于较小(已知)子问题的递归公式。

但是,为了能够使用DP - 您需要具备该问题的一些特征,主要是 - 该问题的最佳解决方案包括其子问题的最佳解决方案。它所持有的示例是shortest-path problem(从st的最佳路径u必须包含从s到{{}的最佳路径1}})。

Vertex-CoverBoolean satisfiability Problem等其他问题上不存在,因此您无法使用DP替换它的回溯解决方案。

答案 3 :(得分:1)

没有。您所谓的回溯缓存基本上是memoization

在动态编程中,你是自下而上的。也就是说,从一个不需要任何子问题的地方开始。特别是,当您需要计算n步时,已经计算了所有n-1步骤。

这不是记忆的情况。在这里,您从k步骤(您想要的步骤)开始,然后在需要时继续解决之前的步骤。显然,将这​​些值存储在某处,以便以后可以访问它们。

所有这些都说,在记忆和动态编程的情况下,运行时间没有差异。