我一直都在想这个。没有书明确说明这一点。
回溯正在探索所有可能性,直到我们发现一种可能性无法引导我们找到可能的解决方案,在这种情况下我们放弃它。
据我所知,动态编程的特点是重叠的子问题。那么,动态编程是否可以表示为缓存回溯(对于以前探索过的路径)?
由于
答案 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 - 1
和n - 2
。
基本状态为n = 0
和n = 1
。
与此订单关系一致的简单总订单是自然顺序:0
,1
,2
,...
。
以下是我们的开始:
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(从s
到t
的最佳路径u
必须包含从s
到{{}的最佳路径1}})。
在Vertex-Cover或Boolean satisfiability Problem等其他问题上不存在,因此您无法使用DP替换它的回溯解决方案。
答案 3 :(得分:1)
没有。您所谓的回溯缓存基本上是memoization。
在动态编程中,你是自下而上的。也就是说,从一个不需要任何子问题的地方开始。特别是,当您需要计算n
步时,已经计算了所有n-1
步骤。
这不是记忆的情况。在这里,您从k
步骤(您想要的步骤)开始,然后在需要时继续解决之前的步骤。显然,将这些值存储在某处,以便以后可以访问它们。
所有这些都说,在记忆和动态编程的情况下,运行时间没有差异。