目录

0. 名字的由来

1. 动态规划的适用条件

1.1 最优子结构(optimal substructure)

1.2 重叠子问题(overlapping subproblems)

1.3  无后效性

1.4 示例1:斐波那契数求解

1.5 示例2:DAG最短路径和

2. 动态规划实现方式

2.1 top-down approach

2.2 bottom-up approach

2.3 top-down和bottom-up的对比

3. 动态规划基本步骤

3.1 实战例1

3.2 实战例2

4. 其它

4.1. 什么是递归[5]

4.2 动态规划与递归的关系

4.3 重叠子问题是不是必须的条件?


0. 名字的由来

动态规划是一个具有广泛应用场景的算法设计范式(相对于转为某一类特定问题而发明的特殊算法),本文基于自己的学习理解做一个简单而概要的描述,以便于自己学而时习之以及累进式提高

动态规划这个名字是如何来的呢?且听一听动态规划算法的发明者怎么说[1]:

 Sometimes a name is just a name:    
“The 1950s were not good years for mathematical research…    I felt I had to do something to shield Wilson and the Air Force from the fact that I was really doing  mathematics...    What title, what name, could I choose?    ...    It's impossible to use the word dynamic in a pejorative sense. Try thinking of some combination that will possibly give it a pejorative meaning.    It's impossible. Thus,  I thought dynamic programming was a good name.  It was something not even a Congressman could object to. So I used it as an umbrella for my   activities.

-- Richard Bellman

嗯,大致就是说,取名动态规划其实就是用一个听起来酷一点的名字去哄哄那些主管拨经费的官员,在这个名字掩护之下做做自己感兴趣的事情(数学)。

1. 动态规划的适用条件

1.1 最优子结构(optimal substructure)

全局最优解能够通过将子问题的最优解以某种方式合成而得,或者说父问题的可以表达为若干个子问题的某种代数关系(加减啊、取最大值啊、根据条件进行选择啊之类的),这个依赖关系称为递推方程,或者状态转移方程。

动态规划某种意义上也是属于分而治之(divide-and-conquer)或者减而治之(decrease-and-conquer)算法策略的一种类型,将大的问题以拆分或者减小规模的方式转化为一个或者多个规模较小的子问题,前提条件是通过对子问题的最优解的融合等处理可以得到原问题的最优解。子问题又可以进一步以相同的方式分解为更小的子问题,直到最后分解到了不能分解的最小的问题,即所谓的基线情况(baseline case)。

1.2 重叠子问题(overlapping subproblems)

寻找原问题的最优解会涉及到反复多次求解同一个子问题的问题。

1.3  无后效性

除了以上两个适用条件外,另一个常被提起的动态规划的适用条件是无后效性。

所谓的无后效性,在随机过程理论中也被称为马尔可夫性。其严格的定义可以表达为:如果给定某一阶段或者time-step的状态,则在这一阶段以后过程的发展不受这阶段以前各段状态的影响。即如何到达当前状态对从当前状态向未来的状态转移没有影响,当前状态本身包含了所有的向向未来的状态转移的所有信息。

反映到动态规划上,无后效性是指一个问题的求解只跟它直接分解出的各子问题的解有关,与各子问题的解是如何求得的没有关系。

1.4 示例1:斐波那契数求解

虽然斐波那契数求解问题有更简单的求解方法,但是因为其简单通俗易懂又满足动态规划所要求的两个前提条件,所以常常被拿来用作动态规划解说例。

以斐波那契数求解问题来说明以上两点。求解fib(6)的问题分解结构可以表示为以下的树的形式[1]。

如上图所示,fib(6)=fib(5) + fib(4),这就是将原问题分解为两个规模较小的子问题fib(5)和fib(4),只要求得了fib(5)和fib(4),然后将它们相加就可以得到fib(6)。而fib(5)和fib(4)又各自可以进一步分解为更小的问题。这就是所谓的最优子结构的特征

进一步,如上图所示,fib(5)和fib(4)的各自的子问题分解构成的子树中有相当多的重复情况存在,事实上,fib(4)本身就是fib(5)的一个子问题。这对应着所谓的重叠子问题的特征。很显然,代表相同的子问题的节点只需要求解一次就可以了。因此,求解fib(6)事实上只需要计算fib(0)、fib(1)、fib(2)、fib(3)、fib(4)、fib(5)各一次就可以了。

最后,fib(0)和fib(1)不能再以fib(n)=fib(n-1)+fib(n-2)的形式进行分解,但是它们的解是显而易见的,或者是基于定义的,在本问题中它们就是所谓的基线情况。

关于无后效性,可以这样理解,对于计算fib(6)而言,只需要有fib(5)和fib(4)的值即可,而至于fib(5)和fib(4)是如何求得的,则对求fib(6)没有影响。事实上,当能够列出明确的递推方程时,就相当于已经确保了无后效性。反之,如果不满足无后效性,则无法列出相应的递推方程或者说状态转移方程。

1.5 示例2:DAG最短路径和

本例参考[4]以及[2]中阮行止的回答(下图取自于后者)。

DAG:Directed Acyclic Graph, 有向无环图

问题要求是求两点之间的最短距离。比如说从S(表示Start)到T点的最短路径。需要注意的是,这里每条边上的数字表示该条边的长度或者路程,由于每条边的路程不相等,因此最短路径(路程和最小)与具有最少边数的路径不一定相同。后者其实是最短路径问题在每条边路程相等的条件下的退化版。我们知道,最少边数的路径问题可以用广度优先搜索算法解决,而一般的最短路径问题也有其特定算法叫做Dijkstra's算法。但是,这里我们不是要将Dijkstra's算法,而是要用动态规划方法来解决它。实际上,动态规划作为一种算法设计范式,适用范围非常广,虽然在一些特定问题上,它的效率比不上具有特定算法的问题(比如说Dijkstra's算法之于本问题),但是很多问题没有这种特定的高效算法,动态规划可能是你唯一的选择!打个比方说,动态规划就像是一种重型坦克,对于非常多类型的问题都可以碾过去,虽然对于有些问题来它显得有些笨拙,不如特定的工具算法那么高效。好,闲话少说,言归正传。

假设我们要求从S到T的距离。从图上我们可以看出,到达T点,只有两种可能,要么从C点过来,要么从D点过来。因此,如果我们记从S到某点的最短路径为dist(x)的话,那么一定满足以下关系:

这个方程就是本问题的递推方程,或者说状态转移方程。

它也代表了以上所说的最优子问题结构,即S到T的最短路径可以分解为两个子问题:从S到C的最短路径问题和从S到D的最短路径问题。而进一步dist(C)和dist(D)的求解也可以以同样的方式进行分解。

其次,从A点既可以到达C点也可以到达D点,这就意味着,dist(C)还是dist(D)的分解都会涉及到dist(A)的求解,也就是存在所谓重叠子问题结构。

第三,对于dist(T)的求解只需要知道dist(D)和dist(C)的最终结果即可,至于选择具体经由什么路劲到达C点或D点是无关紧要的。打个比方说,把上图修改一下,追加或者删除一些节点,或者修改边上的路程值,使得从S到C和D的最短路径(所经历的边的集合)发生了变化,但是路程值没有变化,而且仍然保持只能从C和D到达T,那么dist(T)是不受这些修改的影响的。这就是无后效性。

上式表达的针对dist(T)的递推关系,通用的递推关系为:

其中,V表示所有能到达X的节点,edge(n,X)表示从节点n到达节点X的距离,即该条边上的数值。

2. 动态规划实现方式

动态规划是一种方法论层面的东西,一种解决问题的策略性思维方式,或者说一种套路,一种解决问题的框架,一种范式等等等,落实到具体的实现方式大抵有以下两种:

  1. top-down approach
  2. bottom-up approach

2.1 top-down approach

简而言之,top-down方法就是从最终要解决的原问题出发开始分解,然后逐步按需分解,只求解在分解的路上碰到的需要求解的子问题。

仍以上面斐波那契数求解问题为例,求fib(6)需要求解fib(5)和fib(4),然后求解fib(5)的话需要求解fib(4)和fib(3),求解fib(4)则需要求解fib(3)和fib(2),这样依次从顶到底逐步分解,直到基线情况。

我们可以看出,以上算法描述天然地适合于以递归的方式实现。关于动态规划与递归的关系后面章节还有补充。

但是,在动态规划中,单纯的递归通常都不足以获得最优的性能,因为单纯的递归没有有效地利用动态规划问题的重叠子问题的特征。在斐波那契数求解问题中,如果只是单纯的递归的话,那么就必定会出现大量的子问题被重复计算的情况,函数的递归调用将完全与上图相同,从图中可以看出,fib(2)出现过5次,这就意味着在单纯的递归的实现方式中fib(2)被调用过5次。

以单纯的递归的方式实现斐波那契数求解的话代码如下所示。执行以下程序,可以看出fib(2)的确被调用了5次(追加一条print()语句打印fib被调用的情况即可)。 实现示例代码如下所示:

def fib_recur(k):# baseline caseif k <= 1:return kreturn fib_recur(k-1) + fib_recur(k-2)

如何避免这种重复的无谓的调用呢?

解决方案是所谓的记忆化(memoization)。即将已经计算过的子问题的解保存下来,以备后面需要的时候适用。没有memoization的用单纯的递归来实现动态规划策略在关于子问题的计算方面有点像狗熊掰玉米棒子,掰一个扔一个。所以动态规划的top-down实现方法通常也称为recursion+memoization。

def fib_recur_memo(k):memo = dict()def dp(m):# baseline caseif m <= 1:return mif m in memo:return memo[m]ans = dp(m-1) + dp(m-2)memo[m] = ansreturn ansreturn dp(k)

2.2 bottom-up approach

也成为table-fill方法,即所谓的填表法。

不问青红皂白,从基线情况出发,把到达原求解问题所需要的所有可能的规模较小的子问题全部求解一遍,所得结果存储在一个表中(故得名填表法)--潜台词是:总有一款适合于你

bottom-up方法具体落实到程序实现是采用迭代的程序结构。以斐波那契数计算为例,就是按照fib(2),fib(3),...,的顺序依次计算上去(fib(0),fib(1)的结果由定义而得不需要计算)。

实现示例代码如下:

def fib_iter(k):if k <= 1:return kfib    = (k+1) * [0]fib[1] = 1for i in range(2,k+1):fib[i] = fib[i-1] + fib[i-2]return fib[k]

对以上三个实现代码的时间效率进行一个简单的测试可以获得它们的运行效率的对比的一个基本感性认识:

import time
if __name__ == '__main__':for k in range(0,50,10):tstart = time.time()ans = fib_recur(k)tstop  = time.time()print('fib_recur({0}) = {1}, tcost = {2:5.3f}sec'.format(k, ans, tstop-tstart))for k in range(200,400,40):tstart = time.time()ans = fib_recur_memo(k)tstop  = time.time()print('fib_recur_memo({0}) = {1}, tcost = {2:5.3f}sec'.format(k, ans, tstop-tstart))for k in range(200,400,40):tstart = time.time()ans = fib_iter(k)tstop  = time.time()print('fib_iter({0}) = {1}, tcost = {2:5.3f}sec'.format(k, ans, tstop-tstart))   

fib_recur(0) = 0, tcost = 0.000sec
fib_recur(10) = 55, tcost = 0.000sec
fib_recur(20) = 6765, tcost = 0.008sec
fib_recur(30) = 832040, tcost = 0.640sec
fib_recur(40) = 102334155, tcost = 74.895sec

fib_recur所需要随着k增大而急速增大,呈指数复杂度(更精确地说是),很快就会到达难以承受的程度。而其余两种方法则基本上是线性复杂度,即便到了fib(400)这么大的数也仍然几乎没有花什么时间。

关于递归式实现斐波那契数的计算的时间复杂度分析可以参考:Time complexity of recursive Fibonacci program - GeeksforGeeks

2.3 top-down和bottom-up的对比

一般来说,人们普遍认为top-down的方式更加符合人类的直觉性的思维方式。要解决A,可以先分拆问题B和C;然后要解决B,进一步可以分解为B1和B2两个子问题,问题C进一步可以分解为C1和C2两个子问题;。。。。

top-down用递归的方式实现,通常来说,递归方式的代码编写比较简洁直观易懂,这是因为递归调用中其核心的调用栈都是编译器代劳;bottom-up用迭代的方式,通常代码编写稍微难度高一些。

一般来说,top-down(recursion+memoization)的时间复杂度与bottom-up的填表法是相当的。但是由于前者有一个额外的调用栈管理的overhead,所以虽然是同阶的时间复杂度,但是常数因子通常要比后者大。

另一方面,top-down是按需进行子问题分解,bottom-up是一锅端,把所有的规模比原问题小的子问题都先解决好以表格的方式进行存放以备查询。所以,在有些情况下,top-down有可能所需要求解的子问题的个数会远远小于bottom-up方式,在这种情况下top-down有可能效率比bottom-up高。

很多问题都是既可以用top-down(recursion+memoization)也可以用bottom-up(iteration, table-filling)方法实现。比如说上面举的斐波那契数求解问题以及DAG最短路径问题。

但是,并非所有的动态规划问题都像斐波那契数求解那样有清晰的递推方程或者说状态转移方程,有很多问题,子问题分解不是显式的,无法从一开始就给出明确的描述,这种情况下通常就只能用(或者至少更合适于)采用top-down实现方式。

3. 动态规划基本步骤

  • step1:确定递推关系,或者叫做状态转移方程,即大的问题的解与从其分解的子问题的解的关系。这个方程既包含了问题的分解方式,又包含了如果从子问题的解合成得出父问题的解的方式。
  • step2:确定基线情况
  • step3:选择实现方法,top-down还是bottom-up

以下考虑几个实战例。

3.1 实战例1

3.2 实战例2

4. 其它

4.1. 什么是递归[5]

4.2 动态规划与递归的关系

在这个问题上,人们的意见并不一致,很多人把认为top-down是递归,递归就是递归,只有bottom-up才是动态规划。但是我比较认同的说法是只要满足以上所列的最有子问题结构和重叠子问题以及无后效性等特征并基于此进行算法设计,这个思路本身就是动态规划,至于是top-down还是bottom-up,是递归+记忆化还是填表法,无非是实现的具体形式的差异而已。

在[6]中有很多人给出了精彩的讨论。以下摘抄一些:

###################################################

这两个术语是不同的概念。动态规划属于算法领域,其思想是拆分问题,利用子问题的解(局部最优解)得到原始问题的解(全局最优解)。递归属于程序设计领域,其含义是程序调用自身。

递归的含义是程序调用自身的编程技巧,是一种实现的方式。递归与迭代相对。可以列举常见的简单问题,例如计算阶乘、计算斐波那契数,都可以通过递归或迭代的方式实现。理论上,所有的递归实现都可以使用迭代实现。实现方式和算法之间没有绝对的依赖,同一种算法可能既能用递归实现,也能用迭代实现,例如深度优先搜索,使用递归实现是常见的做法,也可以显性创建一个栈,使用迭代实现。

动态规划是一种算法思想,将原始问题拆分成规模更小且与原始问题性质相同的子问题,利用子问题的解得到原始问题的解。动态规划的适用条件之一是存在重叠子问题。动态规划的实现方式可以是自顶向下或自底向上。使用自顶向下实现时,通常使用递归实现,为了充分利用重叠子问题的性质,需要存储已经计算得到的子问题的答案,这样下次在遇到相同子问题的时候就可以直接得到答案而不需要重复计算。使用自底向上实现时,通常使用迭代实现,此时可以确保每个子问题只会被计算一次而不会被重复计算。

大多数情况下,动态规划都是使用自底向上实现的,而另一种算法则常用自顶向下实现,那就是分治。分治通常使用递归实现,也是将原始问题拆分成规模更小且与原始问题性质相同的子问题,利用子问题的解得到原始问题的解。动态规划和分治的主要区别是重叠子问题的存在以及实现方式。如果存在重叠子问题,使用动态规划可以有效降低时间复杂度和空间复杂度。如果不存在重叠子问题,则可以使用分治。

说明动态规划和分治的区别的一个典型的例子是斐波那契数。斐波那契数的定义是,f(0)=0,f(1)=1,当n>1时f(n)=f(n-1)+f(n-2)。对于给定的n,需要计算对应的斐波那契数f(n)。斐波那契数的问题如果用分治求解,不加任何缓存,则时间复杂度和空间复杂度会高达O(2^n),因此分治显然不是合适的做法。由于存在重复子问题,动态规划是更合适的做法,时间复杂度和空间复杂度都可以降低到O(n),如果使用自底向上的做法,还可以使用空间优化,将空间复杂度降低到O(1)。

回到问题,动态规划是算法思想,可以有多种实现方式,递归是使用自顶向下实现的常用的方式,使用递归的方法解动态规划问题时,为了充分利用重叠子问题的性质,需要存储已经计算得到的子问题的答案。

###################################################

最后补充一点,递归是比动态规划更为底层的概念。递归在DP中用于自顶向下的实现。但是递归还可以用于其它很多地方,比如dfs/bfs这样的搜索场景也可以用递归做。递归还可以用来做模拟,比如A对B做一件事、B又要对C和D做一件事,D又要对A和B做一件事……。用递归来实现很直观。。。。

###################################################

DP最难的不在于自底向上还是自顶向下,而在于怎么拆问题。这个就只有靠多练习了。无它唯手熟尔。。。

###################################################

4.3 重叠子问题是不是必须的条件?

虽然前面提到动态规划适用的两个前提条件之一是重叠子问题结构。但是我对于它是不是必须的有点疑惑。待想清楚了一些再来补充说明。

5. 后记

动态规划学习了有一段时间了,包括在leetcode上也做过很多动态规划的题。有了一些自己的体会,一直想写一个属于自己的总结。但是实际上开始写,才发现脑袋了有一些影影绰绰的东西跟最终把它以逻辑条理清晰的方式变成文字中间还隔着多么遥远的距离。好在总算能够先有一个大致能够自圆其说的东西,以后有机会再回来慢慢地修订补充丰富。

[1] MIT6_0002F16_lec2.pdf

[2] 什么是动态规划(Dynamic Programming)?动态规划的意义是什么?

[3] dynamic programming - What is the difference between bottom-up and top-down?

[4] Sanjoy Dasgupta, 等,《Algorithms》

[5] MIT6_0001F16_Lec6.pdf

[6] 动态规划和递归之间的关系是什么?

动态规划漫谈(面向初学者的自学总结)相关推荐

  1. 自学python买什么书比较好-python官方推荐30本面向初学者的书籍!你看过几本?...

    现在大多数初学者学习python都是看教学视频,但是小编想说的是,如果你能把一本书籍认认真真的读完,那么比你看教学视频的效果要好的多!今天小编就来带大家看看python官方推荐的30本面向初学者的书籍 ...

  2. 关于python的一些好的书籍推荐-python官方推荐30本面向初学者的书籍!你看过几本?...

    现在大多数初学者学习python都是看教学视频,但是小编想说的是,如果你能把一本书籍认认真真的读完,那么比你看教学视频的效果要好的多!今天小编就来带大家看看python官方推荐的30本面向初学者的书籍 ...

  3. 学python买什么书好-python官方推荐30本面向初学者的书籍!你看过几本?

    现在大多数初学者学习python都是看教学视频,但是小编想说的是,如果你能把一本书籍认认真真的读完,那么比你看教学视频的效果要好的多!今天小编就来带大家看看python官方推荐的30本面向初学者的书籍 ...

  4. 数据库初学者_面向初学者的免费6小时数据科学课程

    数据库初学者 Data science is considered the "sexiest job of the 21st century." Learn data scienc ...

  5. thonny python ide_学习用 Thonny 写代码:一个面向初学者的Python IDE

    原标题:学习用 Thonny 写代码:一个面向初学者的Python IDE 编译自: https://fedoramagazine.org/learn-code-thonny-python-ide-b ...

  6. 适合新手的python书籍推荐_推荐一本适合初学者全面自学python的书(附赠电子书)...

    原标题:推荐一本适合初学者全面自学python的书(附赠电子书) 今天一个朋友问我:有个朋友要学习 python,她属于那种特别能啃书的,让我推荐.我学 python 都是无师自通的,没有看过什么书, ...

  7. micropython开发idethonny_Thonny 3.0 首个稳定版发布,一个面向初学者的 Python IDE

    艾米视频电脑版下载,创意表白,蒙口羽绒服,步步高官网,韩剧 black,孙中山后代 在您的既有IT基础设施上按需构建人工智能更高效 Thonny 3.0.1 发布了,这是 Thonny 3.0 系列发 ...

  8. 面向初学者的图形数据库:为什么我们需要NoSQL数据库,ACID与BASE的解释说明

    Table of Contents 为什么我们需要NoSQL数据库 NoSQL数据库的Many&Motley世界 数据量 数据速度 数据种类 数据价 结论 ACID与BASE的解释说明 ACI ...

  9. 面向初学者的带有MVC API的Android 管理表CRUD MSSQL

    目录 介绍 在Android中使用API​​进行表CRUD 屏幕截图示例 第一节 结论 介绍 我的上一篇文章面向初学者的带MVC API的Angular Js Table CRUD MSSQL和使用S ...

最新文章

  1. CCBPM工作流引擎的消息机制与设计
  2. javascript进制转换_44道JavaScript送命题
  3. java移位操作符注意的问题
  4. 发那科pmc地址分配_一台全新的FANUC数控机床,请简述有挡块回参功能的实现步骤?包括PMC的I/O分配、具体参数设定、梯形图程序...
  5. 瑞幸咖啡股价再创新低,App 反冲 TOP 1
  6. 自主巡航——高精度地图制作
  7. 插入排序算法(insertion-sort)
  8. 父与子python下载不了_【求助】看父与子学习Python,里面有一个滑雪小游戏,加载不出图...
  9. GRE 一个月突击攻略
  10. 计算机二级南阳理工学院官网,南阳理工学院外国语学院:彩虹之旅 温暖你心...
  11. 转录组测序分析项目及方法汇总(更新中)
  12. 数组添加/扩容和数组缩减
  13. 低代码技巧:甘特图制作步骤
  14. 如何将720P的mp4视频转换成1080P的视频?视频分辨率如何修改?
  15. 习题3.3 506寝室小组
  16. 反向传播不香了?解读 Hinton 大佬的 Forward-Forward 算法
  17. Java 确定线程池中工作线程数的大小
  18. 【2023亲测有效】Pandownload 归来!加速效果极佳!
  19. 安卓开发:安卓应用上架主流平台汇总
  20. java毕业生设计中小学教务管理平台计算机源码+系统+mysql+调试部署+lw

热门文章

  1. mac查找jdk安装位置
  2. 新媒体广告投放,新媒体广告投放渠道。
  3. 安防CMOS图像传感器市场现状及未来发展趋势
  4. 【算法学习笔记十三】随机算法
  5. 骁龙7gen1和骁龙778g参数对比 骁龙7gen1和骁龙778g哪个好
  6. 永久白嫖,新发现的外卖漏洞,请低调使用
  7. 诺基亚X6 更换充电尾插
  8. 资源 | Python数据分析课程:从入门到实战
  9. 阿里云服务器无法访问tomcat
  10. 收藏了4年的Android 源码分享