Answers:
动态编程是当您使用过去的知识来简化将来的问题时。
一个很好的例子是求解n = 1,000,002的斐波那契数列。
这将是一个非常漫长的过程,但是如果我给您n = 1,000,000和n = 1,000,001的结果怎么办?突然,问题变得更加易于处理。
在字符串问题(例如字符串编辑问题)中,动态编程已被大量使用。您解决问题的一个或多个子集,然后使用该信息来解决更困难的原始问题。
使用动态编程,通常会将结果存储在某种表中。当您需要问题的答案时,请参考表格并查看是否已经知道它是什么。如果不是,则使用表中的数据为自己找到答案的垫脚石。
《 Cormen算法》一书中有很长的一章是关于动态编程的。而且它在Google图书上是免费的!检查它在这里。
动态编程是一种用于避免在递归算法中多次计算相同子问题的技术。
让我们以斐波那契数的简单示例为例:查找由定义的第 n 个斐波那契数
F n = F n-1 + F n-2和F 0 = 0,F 1 = 1
显而易见的方法是递归的:
def fibonacci(n):
if n == 0:
return 0
if n == 1:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
递归进行了很多不必要的计算,因为给定的斐波那契数将被多次计算。改善此问题的一种简单方法是缓存结果:
cache = {}
def fibonacci(n):
if n == 0:
return 0
if n == 1:
return 1
if n in cache:
return cache[n]
cache[n] = fibonacci(n - 1) + fibonacci(n - 2)
return cache[n]
更好的方法是通过按正确的顺序评估结果来完全消除递归:
cache = {}
def fibonacci(n):
cache[0] = 0
cache[1] = 1
for i in range(2, n + 1):
cache[i] = cache[i - 1] + cache[i - 2]
return cache[n]
我们甚至可以使用恒定的空间,并仅存储必要的部分结果:
def fibonacci(n):
fi_minus_2 = 0
fi_minus_1 = 1
for i in range(2, n + 1):
fi = fi_minus_1 + fi_minus_2
fi_minus_1, fi_minus_2 = fi, fi_minus_1
return fi
如何应用动态编程?
动态编程通常适用于具有固有的从左到右顺序的问题,例如字符串,树或整数序列。如果朴素的递归算法不能多次计算相同的子问题,那么动态编程将无济于事。
我收集了一系列问题以帮助理解逻辑:https : //github.com/tristanguigue/dynamic-programing
if n in cache
就像自上而下的示例一样,还是我缺少某些内容?
记忆是存储函数调用的先前结果的时候(给定相同的输入,真实的函数总是返回相同的东西)。在存储结果之前,算法复杂度没有任何区别。
递归是函数调用自身的方法,通常使用较小的数据集。由于大多数递归函数都可以转换为类似的迭代函数,因此这对算法复杂性也没有影响。
动态编程是解决较容易解决的子问题并从中建立答案的过程。大多数DP算法将处于贪婪算法(如果存在)和指数算法(枚举所有可能性并找到最佳方法)之间的运行时间。
这是对算法的优化,可减少运行时间。
虽然贪婪算法通常被称为朴素算法,因为它可能在同一组数据上运行多次,但动态编程通过更深入地了解必须存储以帮助构建最终解决方案的部分结果,避免了这种陷阱。
一个简单的示例是仅遍历将对解决方案做出贡献的节点的树或图,或者将到目前为止已找到的解决方案放入表中,这样就可以避免遍历遍历相同的节点。
这是UVA在线法官提出的适合动态编程的问题示例:“ 编辑梯形图”。
我将简要介绍该问题分析的重要部分,该部分摘自《编程挑战》一书,建议您检查一下。
好好看一下这个问题,如果我们定义一个成本函数来告诉我们两个字符串之间有多远,那么我们有两个考虑三种自然变化类型:
替换-将单个字符从模式“ s”更改为文本“ t”中的其他字符,例如将“ shot”更改为“ spot”。
插入-将单个字符插入模式“ s”以帮助其匹配文本“ t”,例如将“ ago”更改为“ agog”。
删除-从模式“ s”中删除单个字符以帮助其与文本“ t”匹配,例如将“小时”更改为“我们的”。
当我们将每个操作设置为花费一个步骤时,我们将定义两个字符串之间的编辑距离。那么我们如何计算呢?
我们可以通过观察字符串中的最后一个字符必须匹配,替换,插入或删除来定义递归算法。在最后的编辑操作中切掉字符会留下一对操作,会留下一对较小的字符串。令i和j分别为和t的相关前缀的最后一个字符。在最后一个操作之后有三对较短的字符串,对应于匹配/替换,插入或删除之后的字符串。如果我们知道编辑三对较小的字符串的成本,则可以确定哪个选项可导致最佳解决方案,然后相应地选择该选项。我们可以通过很棒的递归来学习这笔费用:
#define MATCH 0 /* enumerated type symbol for match */ #define INSERT 1 /* enumerated type symbol for insert */ #define DELETE 2 /* enumerated type symbol for delete */ int string_compare(char *s, char *t, int i, int j) { int k; /* counter */ int opt[3]; /* cost of the three options */ int lowest_cost; /* lowest cost */ if (i == 0) return(j * indel(’ ’)); if (j == 0) return(i * indel(’ ’)); opt[MATCH] = string_compare(s,t,i-1,j-1) + match(s[i],t[j]); opt[INSERT] = string_compare(s,t,i,j-1) + indel(t[j]); opt[DELETE] = string_compare(s,t,i-1,j) + indel(s[i]); lowest_cost = opt[MATCH]; for (k=INSERT; k<=DELETE; k++) if (opt[k] < lowest_cost) lowest_cost = opt[k]; return( lowest_cost ); }
该算法是正确的,但也可能很慢。
在我们的计算机上运行时,需要花费几秒钟来比较两个11个字符的字符串,并且计算将消失在永远不再存在的地方。
为什么算法这么慢?因为它一次又一次地重新计算值,所以需要花费指数时间。在字符串的每个位置处,递归都以三种方式分支,这意味着它以至少3 ^ n的速度增长–实际上,它的速度甚至更快,因为大多数调用仅减少了两个索引中的一个,而不是两个索引。
那么如何使该算法实用呢?重要的观察结果是,大多数这些递归调用都在计算以前已经计算过的东西。我们怎么知道?好吧,只能有| s |。·| t | 可能存在唯一的递归调用,因为只有许多不同的(i,j)对可以用作递归调用的参数。
通过将每对(i,j)对的值存储在表中,我们可以避免重新计算它们,而只是根据需要查找它们。
该表是二维矩阵m,其中每个| s |·| t | 单元格包含此子问题的最佳解决方案的成本,以及解释我们如何到达此位置的父指针:
typedef struct { int cost; /* cost of reaching this cell */ int parent; /* parent cell */ } cell; cell m[MAXLEN+1][MAXLEN+1]; /* dynamic programming table */
动态编程版本与递归版本有三个差异。
首先,它使用表查找而不是递归调用来获取中间值。
第二,它更新每个单元格的父字段,这将使我们能够在以后重建编辑序列。
**第三,**第三,使用更通用的目标
cell()
函数进行检测,而不仅仅是返回m [| s |] [| t |] .cost。这将使我们能够将此例程应用于更广泛的问题类别。
这里,对收集最佳局部结果所需要采取的一种非常特殊的分析,是使解决方案成为“动态”解决方案。
这是对同一问题的替代完整解决方案。即使执行不同,它也是“动态”的。我建议您将解决方案提交给UVA的在线法官,以检查该解决方案的效率。我发现如何如此有效地解决这样一个沉重的问题感到惊讶。
动态编程的关键是“重叠子问题”和“最优子结构”。问题的这些性质意味着,最佳解决方案由针对其子问题的最佳解决方案组成。例如,最短路径问题表现出最优的子结构。从A到C的最短路径是从A到某个节点B的最短路径,然后是从该节点B到C的最短路径。
更详细地讲,要解决最短路径问题,您将:
由于我们是自下而上地工作,因此,当需要使用子问题时,我们已经通过记住它们来找到解决方案。
请记住,动态编程问题必须同时具有重叠的子问题和最佳子结构。产生斐波那契序列不是一个动态的编程问题;它利用备忘录是因为它具有重叠的子问题,但是它没有最优的子结构(因为不涉及优化问题)。
动态编程
定义
动态编程(DP)是一种用于解决子问题重叠的通用算法设计技术。该技术是美国数学家“理查德·贝尔曼”(Richard Bellman)在1950年代发明的。
关键思想
关键思想是保存较小的子问题重叠的答案,以避免重新计算。
动态编程属性
我对动态编程也非常陌生(针对特定类型问题的强大算法)
用最简单的话来说,只是将动态编程视为一种使用先前知识的递归方法
以前的知识是这里最重要的,请跟踪您已经拥有的子问题的解决方案。
考虑一下这是Wikipedia中dp的最基本示例
找到斐波那契数列
function fib(n) // naive implementation
if n <=1 return n
return fib(n − 1) + fib(n − 2)
让我们用n = 5分解函数调用
fib(5)
fib(4) + fib(3)
(fib(3) + fib(2)) + (fib(2) + fib(1))
((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
(((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
特别是,fib(2)从头开始计算了三遍。在较大的示例中,fib或子问题的更多值被重新计算,从而得出指数时间算法。
现在,通过将我们已经发现的值存储在数据结构(例如Map)中来进行尝试
var m := map(0 → 0, 1 → 1)
function fib(n)
if key n is not in map m
m[n] := fib(n − 1) + fib(n − 2)
return m[n]
如果我们还没有的话,这里我们将其保存在地图中。我们已经计算出的这种保存值的技术称为“记忆化”。
最后,对于一个问题,首先尝试找到状态(可能的子问题,并尝试考虑更好的递归方法,以便可以将先前的子问题的解决方案用于其他问题)。
动态编程是一种用于解决子问题重叠的技术。动态编程算法可一次解决每个子问题,然后将其答案保存在表(数组)中。避免每次遇到子问题时都重新计算答案的工作。动态编程的基本思想是:避免两次计算相同的内容,通常是通过保留子问题的已知结果表来进行。
动态编程算法开发的七个步骤如下:
6. Convert the memoized recursive algorithm into iterative algorithm
强制性步骤吗?这意味着它的最终形式是非递归的吗?
简而言之,递归记忆和动态编程之间的区别
顾名思义,动态编程是使用先前的计算值来动态构建下一个新的解决方案
在哪里进行动态编程:如果您的解决方案基于最佳子结构和子问题重叠,那么在这种情况下,使用较早的计算值将很有用,因此您无需重新计算。这是自下而上的方法。假设您需要计算fib(n),那么您要做的就是将先前计算的值fib(n-1)和fib(n-2)相加
递归:基本上,将您的问题细分为较小的部分即可轻松解决它,但请记住,如果我们在其他递归调用中先前已计算出相同的值,则它不会避免重新计算。
备注:基本上将旧的计算出的递归值存储在表中称为备注,如果先前的某个调用已对其进行了计算,它将避免重新计算,因此任何值都将被计算一次。因此,在计算之前,我们先检查该值是否已经计算出(如果已经计算出),那么我们将从表中返回该值,而不是重新计算。这也是自上而下的方法
下面是一个简单的Python代码示例Recursive
,Top-down
,Bottom-up
为斐波纳契数列的方法:
def fib_recursive(n):
if n == 1 or n == 2:
return 1
else:
return fib_recursive(n-1) + fib_recursive(n-2)
print(fib_recursive(40))
def fib_memoize_or_top_down(n, mem):
if mem[n] is not 0:
return mem[n]
else:
mem[n] = fib_memoize_or_top_down(n-1, mem) + fib_memoize_or_top_down(n-2, mem)
return mem[n]
n = 40
mem = [0] * (n+1)
mem[1] = 1
mem[2] = 1
print(fib_memoize_or_top_down(n, mem))
def fib_bottom_up(n):
mem = [0] * (n+1)
mem[1] = 1
mem[2] = 1
if n == 1 or n == 2:
return 1
for i in range(3, n+1):
mem[i] = mem[i-1] + mem[i-2]
return mem[n]
print(fib_bottom_up(40))