什么是动态编程?

时间:2009-06-30 19:10:24

标签: algorithm dynamic-programming

什么是动态编程

它与递归,记忆等有什么不同?

我已经阅读了wikipedia article,但我仍然不太了解它。

12 个答案:

答案 0 :(得分:190)

动态编程是指您使用过去的知识来更轻松地解决未来问题。

一个很好的例子是解决n = 1,000,002的Fibonacci序列。

这将是一个非常漫长的过程,但如果我给你n = 1,000,000和n = 1,000,001的结果怎么办?突然间,这个问题变得更易于管理。

动态编程在字符串问题中经常使用,例如字符串编辑问题。您解决了问题的一个子集,然后使用该信息来解决更难的原始问题。

通过动态编程,您可以将结果存储在某种表格中。当您需要问题的答案时,您可以参考该表,看看您是否已经知道它是什么。如果没有,您可以使用表格中的数据为自己找到答案的基石。

Cormen算法书有一个关于动态编程的伟大章节。它在谷歌图书上是免费的!查看here.

答案 1 :(得分:140)

动态编程是一种用于避免在递归算法中多次计算相同子问题的技术。

让我们来看一下斐波那契数的简单例子:找出由

定义的n th 斐波纳契数

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
  • 如何应用动态编程?

    1. 找出问题中的递归。
    2. 自上而下:将每个子问题的答案存储在表格中,以避免重新计算它们。
    3. 自下而上:找到合适的订单来评估结果,以便在需要时提供部分结果。

动态编程通常适用于具有固有的从左到右顺序的问题,例如字符串,树或整数序列。如果朴素递归算法不能多次计算相同的子问题,动态编程就不会有帮助。

我制作了一系列问题来帮助理解逻辑:https://github.com/tristanguigue/dynamic-programing

答案 2 :(得分:62)

以下是类似主题中的my answe r

开始

如果你想测试自己,我对在线评委的选择是

当然

您还可以检查优秀的大学算法课程

毕竟,如果你无法解决问题,请问这里存在许多算法瘾君子

答案 3 :(得分:35)

Memoization是存储函数调用的先前结果的时候(实际函数总是返回相同的东西,给定相同的输入)。在存储结果之前,它对算法的复杂性没有任何影响。

递归是调用自身的函数的方法,通常使用较小的数据集。由于大多数递归函数都可以转换为类似的迭代函数,因此这也不会对算法的复杂性产生影响。

动态编程是解决更容易解决的子问题并从中得出答案的过程。大多数DP算法将处于贪婪算法(如果存在)和指数(列举所有可能性并找到最佳可能性)算法之间的运行时间。

  • DP算法可以通过递归实现,但它们不一定是。
  • DP算法无法通过memoization加速,因为每个子问题只能解决一次(或称为“求解”函数)。

答案 4 :(得分:19)

这是优化算法,缩短了运行时间。

虽然贪婪算法通常被称为 naive ,因为它可能在同一组数据上运行多次,但动态编程通过更深入地理解必须存储到的部分结果来避免这种陷阱帮助构建最终解决方案。

一个简单的例子是仅通过可以提供解决方案的节点遍历树或图,或者将目前已找到的解决方案放入表中,这样就可以避免一遍又一遍地遍历相同的节点。

这是一个适合动态编程的问题的例子,来自UVA的在线评判:Edit Steps Ladder.

我将从“编程挑战”一书中快速了解这个问题分析的重要部分,我建议你看一下。

  

如果,请仔细看看这个问题   我们定义一个告诉我们的成本函数   两个字符串到底有多远,我们   有两个考虑三个自然   变化类型:

     

替换 - 换一个   从模式“s”到a的字符   文本“t”中的不同字符,例如   将“镜头”改为“现场”。

     

插入 - 插入单个字符   进入模式“s”以帮助它匹配文本   “t”,例如将“before”改为“agog”。

     

删除 - 删除单个字符   从模式“s”来帮助它匹配文本   “t”,例如将“小时”改为“我们的”。

     

当我们将每个操作设置为   花费一步我们定义编辑   两根弦之间的距离。又怎样   我们计算它吗?

     

我们可以定义一个递归算法   用最后的观察   字符串中的字符必须是   匹配,替换,插入或   删除。砍掉角色   在上一次编辑操作中留下了一个   对操作留下一对   小字符串。让我和j成为   相关前缀的最后一个字符   分别为和。有   三对短弦后   最后一个操作,对应于   匹配/替换后的字符串,   插入或删除。如果我们知道的话   编辑三对的成本   较小的字符串,我们可以决定哪个   选项导致最佳解决方案和   相应地选择该选项。我们可以   通过令人敬畏的方式了解这笔费用   这是递归的事情:

      #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 */
     

动态编程版本有   递归的三个不同之处   版本

     

首先,它使用表查找而不是使用表获取其中间值   递归调用。

     

**其次,**它会更新每个单元格的父字段,这将使我们能够   稍后重建编辑序列。

     

**第三,**第三,使用更通用的目标单元()进行检测   功能而不仅仅是返回   M [| S |] [| T |]。成本。这将使我们   将这个例程应用于更广泛的课程   问题。

在这里,对收集最佳部分结果所需要的非常具体的分析是使解决方案成为“动态”解决方案的原因。

Here's对同一问题的替代完整解决方案。即使它的执行不同,它也是一个“动态”的。我建议您通过将其提交给UVA的在线评委来了解解决方案的效率。我觉得如此有效地处理这么重的问题真是令人惊讶。

答案 5 :(得分:11)

动态编程的关键部分是“重叠子问题”和“最佳子结构”。问题的这些属性意味着最优解决方案由其子问题的最优解决方案组成。例如,最短路径问题表现出最佳子结构。从A到C的最短路径是从A到某个节点B的最短路径,后面是从该节点B到C的最短路径。

更详细地说,要解决最短路径问题,您将:

  • 找到从起始节点到触及它的每个节点的距离(例如从A到B和C)
  • 找到从这些节点到接触它们的节点的距离(从B到D和E,从C到E和F)
  • 我们现在知道从A到E的最短路径:它是我们访问过的某个节点x的A-x和x-E的最短和(B或C)
  • 重复此过程,直到我们到达最终目标节点

因为我们正在自下而上地工作,所以在使用它们时,我们已经通过记忆它们来解决子问题。

请记住,动态编程问题必须同时存在重叠的子问题和最优的子结构。生成Fibonacci序列不是动态编程问题;它利用了memoization,因为它有重叠的子问题,但它没有最佳的子结构(因为没有涉及优化问题)。

答案 6 :(得分:5)

以下是来自CMU的Michael A. Trick的一个教程,我发现它特别有帮助:

http://mat.gsia.cmu.edu/classes/dynamic/dynamic.html

当然除了其他人推荐的所有资源之外(所有其他资源,特别是CLR和Kleinberg,Tardos非常好!)。

我喜欢本教程的原因是因为它逐渐引入了高级概念。它有点古老的材料,但它是这里提供的资源列表的一个很好的补充。

另请参阅Steven Skiena的页面和动态编程讲座: http://www.cs.sunysb.edu/~algorith/video-lectures/

http://www.cs.sunysb.edu/~algorith/video-lectures/1997/lecture12.pdf

答案 7 :(得分:5)

动态编程

<强>定义

动态编程(DP)是一种用于求解的通用算法设计技术 重叠子问题的问题。这项技术是由美国人发明的 数学家“理查德贝尔曼”在20世纪50年代。

关键理念

关键的想法是保存重叠较小的子问题的答案,以避免重新计算。

动态编程属性

  • 使用针对较小实例的解决方案解决实例。
  • 可能需要多次使用较小实例的解决方案, 所以将他们的结果存储在一个表格中。
  • 因此每个较小的实例只解决一次。
  • 额外的空间用于节省时间。

答案 8 :(得分:4)

我也非常喜欢动态编程(针对特定类型问题的强大算法)

简单来说,只需将动态编程视为使用以前的知识的递归方法

以前的知识是最重要的,跟踪您已经存在的子问题的解决方案。

考虑这个,来自维基百科的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]

如果我们还没有将问题解决方案保存在地图中,我们就在这里。这种保存我们已经计算过的值的技术被称为Memoization。

最后,对于一个问题,首先尝试找到状态(可能的子问题,并尝试考虑更好的递归方法,以便您可以使用先前子问题的解决方案进一步解决)。

答案 9 :(得分:2)

动态编程是一种解决具有重叠子问题的问题的技术。 动态编程算法可以一次解决所有子问题 将其答案保存在表(数组)中。 避免每次遇到子问题时都重新计算答案的工作。 动态编程的基本思想是: 避免计算两次相同的数据,通常是通过保留子问题的已知结果表来进行。

开发动态规划算法的七个步骤如下:

  1. 建立一个递归属性,为问题实例提供解决方案。
  2. 根据递归属性开发递归算法
  3. 看看问题的相同情况是否在递归调用中再次得到解决
  4. 开发一种记忆式递归算法
  5. 查看将数据存储在内存中的模式
  6. 将记忆式递归算法转换为迭代算法
  7. 通过按需使用存储来优化迭代算法(存储优化)

答案 10 :(得分:1)

简而言之,递归记忆和动态编程之间的区别

动态编程名称建议使用先前计算的值来动态构建下一个新解决方案

应用动态编程的位置:如果解决方案基于最佳子结构和重叠子问题,那么在这种情况下使用较早的计算值将非常有用,因此您不必重新计算它。这是自下而上的方法。假设您需要计算fib(n),那么您需要做的就是添加先前计算的fib(n-1)和fib(n-2)的值

递归:基本上将您的问题细分为较小的部分以轻松解决它但请记住,如果我们先前在其他递归调用中计算了相同的值,则不会避免重新计算。

记忆:基本上将旧计算的递归值存储在表中称为记忆,如果已经通过某个先前的调用计算了它,则将避免重新计算,因此任何值都将被计算一次。所以在计算之前我们检查这个值是否已经计算过,如果已经计算过,那么我们从表中返回相同的值而不是重新计算。它也是自上而下的方法

答案 11 :(得分:-2)

以下是斐波那契系列的RecursiveTop-downBottom-up方法的简单python代码示例:

递归:O(2 ^ n)

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))

自上而下:O(n)对较大输入有效

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))

自下而上:O(n)为简单起见,输入尺寸小

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))