动态规划阐述

Posted 编程男孩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划阐述相关的知识,希望对你有一定的参考价值。

动态规划(DP)算法一般只能应用于具有最优子结构的问题。当我考虑一个问题是否能用DP求解时,我们首先要考虑这个问题是否具有以下两个特性:

  • 重叠子问题(Overlapping SubProblems)

    如果一个问题能被分解成N个子问题,并且每个子问题之间不是相互独立,一个子问题的解可能在另一个子问题的求解过程中被重复用到,那我们可以说这个问题具有重叠子问题的特性。

  • 最优子结构(Optimal Subproblems)

    如果一个问题的最优解能通过使用它的子问题的最优解来求得,我们就说这个问题有最优子结构,换句话说就是原问题的最优解包括了它子问题的最优解。




重叠子问题

先回顾一下我们上一篇文章中求解斐波那契数列的那张图:

在这张图中我们可以看到,如果我们不用DP算法的话,红色标记的部分就是重叠子问题,这些子问题的结果我们在其他地方已经计算过,是可以被直接复用的。这里要说明的很多问题都可以被分解成子问题求解(分治思想),例如二分查找,但在二分查找中,我们并没有找到会被重复计算的子问题,也就是说它的子问题不具有重叠性,所以它并不适用DP算法。

由于子问题具有重叠性,所以我们需要把这些子问题的结果缓存起来以防将来用到,这里有两种思路来存储子问题的结果,分别是:自顶向下的Memoization和自底向上的Tabulation


Memoization

这个思路其实只是对斐波那契的递归解法的一种改造,首先我们会初始化一个备忘录,我们也可以称它为查找表(lookup table),当我们需要解决某个子问题时,可以先在备忘录中查找一下有没有现成的解可以直接拿来用,如果有的话就直接用;如果没有的话我们就计算出这个子问题的解,然后把它存放在备忘录中以方便后续使用。

let lookup = []function fib(n){ // 备忘录中没找到,把子问题的计算结果存进去 if(!lookup[n]){ if(n <= 1){ lookup[n] = 1 } else { lookup[n] = fib(n-1) + fib(n-2) } } // 备忘录中已经有可以直接用的子问题的解 return lookup[n]}

这种方式其实还是递归,递归的思路其实就是从结果反推,从行为上看就是自顶下向的方式求解。




Tabulation

这个单词直译过来就是做表格的意思,我们后续求解其他的DP问题会经常遇到这个操作,这里先了解一下。还以斐波那契数列为例,如果我们想知道fib(3)是多少,那我们需要先算出fib(1)和fib(2),然后把它们依次放入一个表里(这个表只是一个抽象的概念),然后我们可以利用fib(1)和fib(2)加起来算出fib(3)并把它也放入表中,最后我们返回表里的最后一个(顶部)数据就是我们想要的结果。

function fib(n) { const f = [] f[0] = 0 f[1] = 1 for(let i=2; i<=n; i++){ f[i] = f[i-1] + f[i-2] } return f[n]}

Memoization和Tabulation这两种方式都是把子问题的解缓存起来,不同的是缓存时机。Tabulation很好理解,就是从头开始不停地计算,并把结果一个接一个的存起来;Memoization则是在需要某个子问题的解,但现在还没有的时候才会去计算然后缓存起来,比如当我计算fib(5)时,我需要用到fib(4),但此时fib(4)还不存在,此时我们就要把fib(4)算出来并把它存进lookup中。





不知不觉写的有点多了,最优子结构问题我们放在下一篇再说吧。


以上是关于动态规划阐述的主要内容,如果未能解决你的问题,请参考以下文章

干货:图解算法——动态规划系列

干货:图解算法——动态规划系列

详细实例说明+典型案例实现 对动态规划法进行全面分析 | C++

动态规划——从入门到放弃

递归详解

动态 Rstudio 代码片段