循序渐进,搞懂什么是动态规划
写在前面
温馨提示,本文的篇幅很长,需要花很长的时间阅读。如果要完全理解所有内容,还需要花更多的时间学习。如果打算认真学习动态规划,又不能一次看完,建议您收藏本文以便后续回来看,作为学习的参考。
要学习和理解动态规划,不太可能通过碎片时间完全学会,需要花很多时间认真学习。本文的内容尽量避免了各种复杂的公式推导过程,重在理解动态规划的思想和方法。如果要精通动态规划,需要多结合实战。
动态规划是什么
动态规划(dynamic programming)是用来求解最优化问题的一种方法,是一种解决问题的思想。因为英文是 dynamic programming ,所以动态规划常简称DP,在《算法导论》中说,此处“programming”是指一种规划,而不是指写计算机代码。
在《运筹学》和《算法导论》中都有专门的章节介绍动态规划。由于两本书面向的学科不同,所以着重点有一些差异,不过在两本书中动态规划的思想是完全一致的。本文后面会将两本书中动态规划章节的部分内容摘录下来,供大家阅读参考。
《运筹学》中介绍,有一类最优化问题分为若干个互相联系的阶段,每一个阶段都需要作出决策,当各个阶段的决策确定后,就组成了一个决策序列。这种问题可看做是一个前后关联具有链状结构的多阶段决策问题,各阶段的决策既依赖于当前的状态,又随即引起状态的转移,一个决策序列是在变化的状态中产生的,故有“动态”的含义。
《算法导论》中介绍,动态规划类似于分治法,将一个问题拆成几个子问题,分别求解这些子问题,通过组合子问题的解而解决整个问题。但动态规划又不完全与分治法一样,分治法是将问题分成一些独立的子问题,递归地求解各子问题。而动态规划适用于子问题不独立的情况,也就是子问题包含了公共的子子问题。分治法需要重复求解每一个子子问题,而动态规划对每个子子问题只求解一次。
关于每个子子问题只求解一次这一点,类似于将每个子子问题的解保存在一个表中,需要反复计算时直接查表,做到每个子问题只解一次。效果类似于带缓存(“备忘录”)的递归,类似于记忆化搜索,参考:Python中的@cache有什么妙用?。但是,动态规划中的实现方式并不完全与缓存相同,因为递归与动态规划的求解顺序(遍历顺序与递归顺序)大多数时候是相反的。
在代码开发中使用动态规划时,说的动态规划更接近《算法导论》中的动态规划,这也是为什么动态规划经常被当成一种算法。
动态规划的关键概念
1.重叠子问题
动态规划是将一个多阶段问题分成几个相互联系的单阶段问题。这几个相互联系的单阶段问题一定要是同类型的子问题,这样分成多阶段决策才有意义,否则每次面对的都是不同的问题,也就失去了分成单阶段的意义。在《算法导论》中,把这种同类型的子问题叫做”重叠子问题“。重叠不是完全相同,而是同类型,可以通过解决子问题,然后合并子问题的解而得到原问题的解。
动态规划中子问题的“重叠”,体现为可以用一个多项式来表示每个子问题。利用子问题的初始解和递推多项式,可以求出每个子问题的解,最终求出整个问题的解。
动态规划要求其子问题既要独立又要重叠,这看上去似乎有些奇怪。虽然这两点要求听起来可能是矛盾的,但它们描述了两种不同的概念,而不是同一个问题的两个方面。如果同一问题的两个子问题不共享资源,则它们就是独立的。对两个子问题来说,如果它们确实是相同的子问题,只是作为不同问题的子问题出现的话,则它们是重叠的。
把自顶向下的递归算法与自底向上的动态规划算法做比较,可以看出后者更加有效,因为它利用了重叠子问题的性质。动态规划算法对每一个子问题只解一次,递归算法对在递归树中重复出现的每个子问题都要重复解一次。当某个问题的自然递归解的递归树中反复包含同一个子问题,而且不同的子问题个数很小,可以考虑能否用动态规划来解这个问题。
2.最优子结构
用动态规划求解最优化问题的必要条件是描述最优解的结构。如果问题的一个最优解中包含了子问题的最优解,则该问题具有最优子结构。动态规划用于求解最优化问题,且问题的最优解中包括了子问题的最优解,所以具有“最优子结构”。
当一个问题具有最优子结构时,提示我们动态规划可能会适用。动态规划以自底向上的方式来利用最优子结构,首先找到子问题的最优解,解决子问题,然后找到问题的一个最优解。也就是说,求解小问题,然后推出大问题的解。
利用一种“剪贴”(cut-and-paste)技术,可以证明在问题的一个最优解中,使用的子问题的解本身也必须是最优的。使用反证法,假设每一个子问题的解都不是最优解,通过“剪除”非最优的子问题解再“贴上”最优的子问题解,就证明了可以得到原问题的一个更好的解,因此,这与已经得到问题的一个最优解相矛盾。
3.无后效性
无后效性又称为马尔可夫性(Markov property),是一个概率论中的概念。无后效性是指如果给定某一阶段的状态,则此阶段以后过程的发展仅与此阶段的状态有关,而不受此阶段以前各阶段状态的影响。通常浓缩成一句简短的“未来与过去无关”。问题的历史状态不影响未来的发展,只能通过当前的状态去影响未来,当前的状态是以往历史的一个总结。
在利用动态规划方法求解多阶段决策问题时,过程的状态必须具有无后效性。具体地说,如果k阶段的值已经确定,则在k+1阶段只关心k阶段的值,而不用关心k阶段的值是怎么求出来的,也不用关心k阶段以前的阶段的值。
4.状态描述和初始状态
在使用动态规划解决问题时,要能正确地描述最优解的结构,对最优解的结构进行准确描述称为“状态描述”。状态描述确定后,可以根据描述找到子问题的初始值(边界值),也即初始状态,问题的最终解将依据初始状态逐步推导得出。
状态描述对解决问题至关重要,且它并没有看起来那么简单,在解决千变万化的实际问题时,经常需要复杂的分析才能构造出最优解的结构。因为对最优化问题的状态描述需要满足最优子结构和无后效性,才能满足用动态规划解决问题的前提。
5.状态转移
动态规划将问题分解成多个阶段的子问题,并定义了每一个阶段的状态描述,在问题求解过程中,每个状态均可以由前序的状态推导得出,这种由前序状态推导出当前阶段状态的过程称为状态转移。
如果要利用前序阶段子问题的结果解决现阶段的子问题,必须要能够建立前后阶段状态的转移关系。状态转移的转移方式通常可以用问题规模的多项式来表示,描述状态转移的多项式称为状态转移方程,也可以称为递推公式。状态转移方程确定了由一个状态到另一个状态的演变过程,列出状态转移方程也是求解动态规划问题中的一个关键点和难点。
动态规划的一般求解步骤
动态规划问题的核心步骤有定义问题的状态、列出状态转移方程、状态初始化和代码求解,不管你如何拆分求解步骤,这几步都是必不可少的。
1.定义问题的状态
定义问题的状态是要明确状态表示什么含义,定义的状态需要满足最优子结构和无后效性,如果问题的状态定义得不对,会导致无法用动态规划来求解。
具体到代码中,就是要定义 dp 数组中每个元素的含义,以及 dp 数组中下标的含义。代码中一般都会用一个数组来存储所有子问题状态的值,这个数组可以是一维数组 dp[i] 或二维数组 dp[i][j] 等。明确状态的含义是列出状态转移方程和问题求解的前提。
2.列出状态转移方程
状态转移方程是指从前序状态推导出当前状态的推导关系,通常可以用问题规模的多项式来表示,所以也常称为递推公式。列出正确的状态转移方程,是动态规划问题中非常重要的一个步骤,也是一个比较困难的步骤。
具体到代码中,就是找到 dp[i] 与 dp[i-1],dp[i-2], … 的关系,用 dp[i-1],dp[i-2], … 来表示 dp[i] 。例如,用 dp[i] 表示 i 的阶乘,则 dp[i] = i * dp[i-1],用 dp[i] 表示斐波那契数列,则 dp[i] = dp[i-1] + dp[i-2] 。递推公式在不同问题中是千变万化的,所以要分析清楚问题,从定义状态时就开始考虑递推公式的推导,假如无法得到递推公式,可以重新分析问题的状态定义得是否适合。
3.状态初始化
在定义完状态和找到递推公式后,需要求解问题的初始值,才能有递推解决问题的起点。动态规划的每一个状态都是从前序状态推导而来的,所以一定有初始值,也就是状态转移的起点。不过,起始状态并不是一成不变的,要根据定义的状态,找到有意义的初始值。
具体到代码中,求初始值就是求出起始状态的值。例如,阶乘的初始值是 dp[1] = 1,斐波那契数列的初始值是 dp[1] = 1, dp[2] = 1。在具体问题中,有些问题的初始值是从下标 1 开始,有些问题的初始值是从下标 0 开始,还有些问题的初始值是从 n-1 开始,等等。有些问题需要一个初始值,有些问题需要多个初始值。所以在求初始值时要根据状态定义和递推公式来调整,具体问题具体分析。
4.代码求解
前面的三个步骤主要是对问题进行剖析,找到解决问题的方法。
完成前面的步骤后,我们需要根据思路编写代码、运行代码得到结果。相对于前面三步而言,编写代码其实是最简单的一步,动态规划的代码通常像套公式一样,因为只要经过了前面的分析,写代码只是把结果计算出来。虽然说,不编写代码并执行,就得不到结果,但是在动态规划中,关键在于解题思路,很多人甚至没有把编写代码当成一个步骤。
动态规划实际举例
看完前面的理论描述,如果你还一头雾水,其实不用自我怀疑,理解动态规划通常都不是一蹴而就的,下面来看一个动态规划的举例:爬楼梯问题,希望能帮助你加深对动态规划的理解。
问题描述
假设你正在爬楼梯,需要爬 n 个台阶你才能到达楼顶。
每步你可以爬1个或2个台阶,你有多少种不同的方法可以爬到楼顶呢?
问题分析和求解
爬楼梯问题中,求解的是爬 n 个台阶有多少种方法,而每步可以爬1阶或2阶。先反过来推演,寻找思路,首先,爬楼梯是一步一步地爬,如果你只差一步就能到第 n 阶,有两种情况,要么你从第 n-1 阶爬1阶到达第 n 阶,要么你从第 n-2 阶爬2阶到达第 n 阶。所以假如知道你到达第 n-1 阶有多少种方法和你到达第 n-2 阶有多少种方法,那到达第 n 阶的方法就是这两种情况相加。(一定要理解这段分析,这是解决问题的关键,尤其注意,从第 n-2 阶爬两步1阶到达第 n 阶这种情况已经包含在从第 n-1 阶爬一步1阶到达第 n 阶里了。核心思想是,只爬一步就到达第 n 阶分两种情况,如果知道这两种情况分别有多少种方法,将它们相加就能得到爬到第 n 阶的所有方法。)
问题分析得差不多了,按照上面说的步骤,逐步来看求解过程。
步骤一、定义问题的状态
根据分析,将状态定义为爬到第 n 阶台阶的方法数,也就是 dp[n] 表示爬到第 n 阶的方法数。根据前面的分析,dp[n] 与 dp[n-1] 和 dp[n-2] 相关,且不用管 dp[n-1] 和 dp[n-2] 是如何求出的,满足无后效性。同时,在求有多少种方法时,隐含了“最多”的含义,当前问题的最优解包含了子问题的最优解,满足最优子结构。
步骤二、列出状态转移方程
根据前面的分析,已经可以得出状态转移方程。爬楼梯问题的状态转移方程为:dp[n] = dp[n-1] + dp[n-2] 。
步骤三、状态初始化
在爬楼梯问题中,要爬 n 阶台阶,所以在开始爬之前,你的位置是在第 1 阶的下面,也就是第 0 阶。但是初始化并不是从 0 开始初始化,因为状态的定义是爬到第 n 阶有多少种方法,这个 n 是表示你爬之后的位置,所以问题中就隐含了 n >= 1 的条件,爬到第 0 阶这种情况是不存在的,没有实际意义。
从1开始初始化,爬到第 1 阶只有一种方法,dp[1] = 1 ,爬到第 2 阶有两种方法,dp[2] = 2 。
步骤四、代码求解
本文代码用 Python 语言,代码如下:
def climb_stairs(n):
if n == 1:
return 1
dp = [0] * n
dp[0], dp[1] = 1, 2
for i in range(2, n):
dp[i] = dp[i-1] + dp[i-2]
return dp[n-1]
print(climb_stairs(10))
代码完成,假如需要爬10级台阶,共有89种方法,顺利求出问题的解。这个问题在力扣上也有(第70题),提交这个代码可以通过所有用例。当然,上面这段代码还可以对空间复杂度进行优化,为了体现动态规划的求解步骤,这段代码是按上面的思路来写的。
爬楼梯这个例子是非常简单的动态规划案例,主要是为了帮助理解动态规划的解题思路,实际问题往往比这个复杂得多,如果要加深对动态规划的理解,还需要多多实战。
下面将《运筹学》和《算法导论》中对动态规划的介绍附在最后,希望能对你有帮助。
《运筹学》和《算法导论》中的动态规划
在《运筹学》和《算法导论》中都有动态规划,这两本书中对动态规划的介绍有差异的部分,但整体思路是完全相通的,书中提供了完整的基本概念和知识,部分内容摘录如下。
《运筹学》中的动态规划:
多阶段决策过程
在生产和科学实验中,有一类活动的过程,由于它的特殊性,可将过程分为若干个互相联系的阶段,在它的每一个阶段都需要作出决策,从而使整个过程达到最好的活动效果。因此,各个阶段决策的选取不是任意确定的,它依赖于当前面临的状态,又影响以后的发展。当各个阶段决策确定后,就组成了一个决策序列,因而也就决定了整个过程的一条活动路线。这种把一个问题可看做是一个前后关联具有链状结构的多阶段过程就称为多阶段决策过程,也称序贯决策过程。这种问题就称为多阶段决策过程。
在多阶段决策问题中,各个阶段采取的决策,一般来说是与时间有关的,决策依赖于当前的状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,故有“动态”的含义。因此,把处理它的方法称为动态规划方法。但是,一些与时间没有关系的静态规划(如线性规划、非线性规划等)问题,只要人为地引进“时间”因素,也可把它视为多阶段决策问题,用动态规划方法去处理。
多阶段决策问题很多,如最短路线问题,机器负荷分配问题,各种资源(人力、物力)分配问题,生产-存储问题,最优装载问题,水库优化调度问题,最优控制问题等等,都是具有多阶段决策问题的特性,均可用动态规划方法去求解。
动态规划的基本概念
1. 阶段
把所给问题的过程,恰当地分为若干个相互联系的阶段,以便能按一定的次序去求解。描述阶段的变量称为阶段变量,常用 k 表示。阶段的划分,一般是根据时间和空间的自然特征来划分,但要便于把问题的过程能转化为多阶段决策的过程。
2. 状态
状态表示每个阶段开始所处的自然状况或客观条件,它描述了研究问题过程的状况,又称不可控因素。通常一个阶段有若干个状态,一般第 k 阶段的状态就是第 k 阶段所有始点的集合。
描述过程状态的变量称为状态变量。它可用一个数、一组数或向量(多维情形)来描述。常用 Sk 表示第 k 阶段的状态变量。一个阶段所有可能的状态取值组成的集合称为该阶段的可达状态集合,第 k 阶段的可达状态集合就记为 Sk。
这里所说的状态应具有下面的性质:如果某阶段状态给定后,则在这阶段以后过程的发展不受这阶段以前各段状态的影响。换句话说,过程的过去历史只能通过当前的状态去影响它未来的发展,当前的状态是以往历史的一个总结。这个性质称为无后效性(即马尔可夫性)。
如果状态仅仅描述过程的具体特征,则并不是任何实际过程都能满足无后效性的要求。所以在构造决策过程的动态规划模型时,不能仅由描述过程的具体特征这点着眼去规定状态变量,而要充分注意是否满足无后效性的要求。如果状态的某种规定方式可能导致不满足无后效性,应适当地改变状态的规定方法,达到能使它满足无后效性的要求。
3. 决策
决策表示当过程处于某一阶段某个状态时,可以作出不同的决定(或选择),从而确定下一阶段的状态,这种决定称为决策。在最优控制中也称为控制。描述决策的变量,称为决策变量。它可用一个数、一组数或向量来描述。常用 uk(sk) 表示第 k 阶段当状态处于 sk 时的决策变量,它是状态变量的函数。在实际问题中,决策变量的取值往往限制在某一范围之内,此范围称为允许决策集合。常用 Dk(sk) 表示第 k 阶段从状态 sk 出发的允许决策集合,显然有 uk(sk)∈Dk(sk) 。
4. 策略
策略是一个按顺序排列的决策组成的集合。由过程的第 k 阶段开始到终止状态为止的过程。称为问题的后部子过程(或称为 k 子过程)。由每段的决策按顺序排列组成的举措函数序列称为 k 子过程策略,简称子策略。当 k=1 时,决策函数序列称为全过程的一个策略,简称策略。
在实际问题中,可供选择的策略有一定的范围,此范围称为允许策略集合,用 P 表示。从允许策略集合中找出达到最优效果的策略称为最优策略。
5. 状态转移方程
状态转移方程是确定过程由一个状态到另一个状态的演变过程。若给定第 k 阶段状态变量 sk 的值,如果该阶段的决策变量 uk 一经确定,第 k+1 阶段的状态变量 sk+1 的值也就完全确定。即 sk+1 的值随 sk 和 uk 的值变化而变化。这种确定的对应关系,记为 sk+1 =Tk(sk,uk)。此式描述了由 k 阶段到 k+1 阶段的状态转移规律,称为状态转移方程。Tk 称为状态转移函数。
6. 指标函数和最优值函数
用来衡量所实现过程优劣的一种数量指标,称为指标函数。它是定义在全过程和所有后部子过程上确定的数量函数。常用 Vk,n 表示。对于要构成动态规划模型的指标函数,应具有可分离性,并满足递推关系。
指标函数的最优值,称为最优值函数。它表示从第 k 阶段的状态 sk 开始到第 n 阶段的终止状态的过程,采用最优策略所得到的指标函数值。
在不同的问题中,指标函数的含义是不同的,它可能是距离、利润、成本、产品的产量或资源消耗等。
动态规划的基本思想和基本方程
1.动态规划方法的关键在于正确地写出基本的递推关系式和恰当的边界条件(简言之为基本方程)。要做到这一点,必须先将问题的过程分成几个互相联系的阶段,恰当地选取状态变量和决策变量及定义最优值函数,从而把一个大问题化成一族同类型的子问题,然后逐个求解。即从边界条件开始,逐段递推寻优,在每一个子问题的求解中,均利用了它前面的子问题的最优化结果,依次进行,最后一个子问题所得的最优解,就是整个问题的最优解。
2.在多阶段决策过程中,动态规划方法是既把当前一段和未来各段分开,又把当前效益和未来效益结合起来考虑的一种最优化方法。因此,每段决策的选取是从全局来考虑的,与该段的最优选择答案一般是不同的。
3.在求整个问题的最优策略时,由于初始状态是已知的,而每段的决策都是该段状态的函数,故最优策略所经过的各段状态便可逐次变换得到,从而确定了最优路线。
给一个实际问题建立动态规划模型时,必须做到下面五点:
(1)将问题的过程划分成恰当的阶段。
(2)正确选择状态变量 sk ,使它既能描述过程的演变,又要满足无后效性。
(3)确定决策变量 uk 及每阶段的允许决策集合 Dk(sk)。
(4)正确写出状态转移方程。
(5)正确写出指标函数的关系,它应满足下面三个性质:
- 定义在全过程和所有后部子过程上的数量函数;
- 具有可分离性,并满足递推关系;
- 函数要严格单调。
以上五点是构造动态规划模型的基础,是正确写出动态规划基本方程的基本要素。而一个问题的动态规划模型是否正确给出,它集中地反映在恰当的定义最优值函数和正确地写出递推关系式及边界条件上。简言之,要正确写出动态规划的基本方程。
以上内容摘自清华大学出版社《运筹学(第4版)》第7章:动态规划。为了便于理解,省略了具体的举例和复杂的推导公式等内容。如果您有空,建议多花点时间去认真阅读原书。
《算法导论》中的动态规划:
和分治法一样,动态规划(dynamic programming)是通过组合子问题的解而解决整个问题的。(此处“programming”是指一种规划,而不是指写计算机代码。)分治法算法是指将问题划分成一些独立的子问题,递归地求解各子问题,然后合并子问题的解而得到原问题的解。与此不同,动态规划适用于子问题不是独立的情况,也就是各子问题包含公共的子子问题。在这种情况下,若用分治法则会做许多不必要的工作,即重复地求解公共的子子问题。动态规划算法对每个子子问题只求解一次,将其结果保存在一张表中,从而避免每次遇到各个子问题时重新计算答案。
动态规划通常应用于最优化问题。此类问题可能有很多种可行解。每个解有一个值,而我们希望找出一个具有最优(最大或最小)值的解。称这样的解为该问题的“一个”最优解(而不是“确定的”最优解),因为可能存在多个取最优值的解。
动态规划算法的设计可以分为如下4个步骤:
- 描述最优解的结构。
- 递归定义最优解的值。
- 按自底向上的方式计算最优解的值。
- 由计算出的结果构造一个最优解。
第1~3步构成问题的动态规划解的基础。第4步在只要求计算最优解的值时可以略去。如果的确做了第4步,则有时要在第3步的计算中记录一些附加信息,使构造一个最优解变得容易。
动态规划基础
在本节中,我们要介绍适合采用动态规划方法的最优化问题中的两个要素:最优子结构和重叠子问题。另外,还要分析一种不同的方法,称为做备忘录(memoization),以充分利用重叠子问题性质。
最优子结构
用动态规划求解优化问题的第一步是描述最优解的结构。回顾一下,如果问题的一个最优解中包含了子问题的最优解,则该问题具有最优子结构。当一个问题具有最优子结构时,提示我们动态规划可能会使用。在动态规划中,我们利用子问题的最优解来构造问题的一个最优解。因此,必须小心以确保在我们所考虑的子问题范围中,包含了用于一个最优解中的哪些子问题。
在找寻最优子结构时,可以遵循一种共同的模式:
1)问题的一个解可以是做一个选择,做这种选择会得到一个或多个有待解决的子问题。
2)假设对一个给定的问题,已知的是一个可以导致最优解的选择。不必关心如何确定这个选择,尽管假定它是已知的。
3)在已知这个选择后,要确定哪些子问题会随之发生,以及如何最好地描述所得到的子问题空间。
4)利用一种“剪贴”(cut-and-paste)技术,来证明在问题的一个最优解中,使用的子问题的解本身也必须是最优的。通过假设每一个子问题的解都不是最优解,然后导出矛盾,即可做到这一点。特别地,通过“剪除”非最优的子问题解再“贴上”最优解,就证明了可以得到原问题的一个更好的解,因此,这与假设已经得到一个最优解相矛盾。如果有多于一个的子问题的话,由于它们通常非常类似,所以只要对其中一个子问题“剪贴”处理略加修改,即可很容易地用于其他子问题。
为了描述子问题空间,可以遵循这样一条有效的经验规则,就是尽量保持这个空间简单,然后在需要时再扩充它。
最优子结构在问题域中以两种方式变化:
1)有多少个子问题被使用在原问题的一个最优解中。
2)在决定一个最优解中使用哪些子问题时有多少个选择。
非正式地,一个动态规划算法的运行时间依赖于两个因素的乘积:子问题的总个数和每一个子问题中有多少种选择。
动态规划以自底向上的方式来利用最优子结构。也就是说,首先找到子问题的最优解,解决子问题,然后找到问题的一个最优解。寻找问题的一个最优解需要在子问题中做出选择,即选择将用哪一个来求解问题。问题解的代价通常是子问题的代价加上选择本身带来的开销。
重叠子问题
适用于动态规划求解的最优化问题必须具有的第二个要素是子问题的空间要“很小”,也就是用来解原问题的递归算法可反复地解同样的子问题,而不是总在产生新的子问题。典型地,不同的子问题数是输入规模的一个多项式。当一个递归算法不断地调用同一问题时,我们说该最优问题包含重叠子问题。相反地,适合用分治法解决的问题往往在递归的每一步都产生全新的问题。动态规划算法总是充分利用重叠子问题,即通过每个子问题只解一次,把解保存在一个在需要时就可以查看的表中,而每次查表的时间为常数。
动态规划要求其子问题既要独立又要重叠,这看上去似乎有些奇怪。虽然这两点要求听起来可能是矛盾的,但它们描述了两种不同的概念,而不是同一个问题的两个方面。如果同一问题的两个子问题不共享资源,则它们就是独立的。对两个子问题来说,如果它们确实是相同的子问题,只是作为不同问题的子问题出现的话,则它们是重叠的。
把自顶向下的递归算法与自底向上的动态规划算法做比较,可以看出后者更加有效,因为它利用了重叠子问题的性质。动态规划算法对每一个子问题只解一次,递归算法对在递归树中重复出现的每个子问题都要重复解一次。当某个问题的自然递归解的递归树中反复包含同一个子问题,而且不同的子问题个数很小,可以考虑能否用动态规划来解这个问题。
做备忘录
动态规划有一种变形,它即具有通常的动态规划方法的效率,又采用了一种自顶向下的策略。其思想就是备忘(memoize)原问题的自然但低效的递归算法。像在通常的动态规划中一样,维护一个记录子问题解的表,但有关填表动作的控制结构更像递归算法。
加了备忘的递归算法为每一个子问题的解在表中记录一个表项。开始时,每个表项最初都包含一个特殊的值,以表示该表项有待填入。当在递归算法的执行中第一次遇到一个子问题时,就计算它的解并填入表中。以后每次遇到该子问题时,只要查看并返回表中先前填入的值即可。
在实际应用中,如果所有的子问题都至少要被计算一次,则一个自底向上的动态规划算法通常要比一个自顶向下的做备忘录算法好出一个常数因子,因为前者无需递归的代价,而且维护表格的开销也小些。此外,在有些问题中,还可以用动态规划算法中的表存取模式来进一步减少时间或空间上的需求。或者,如果子问题空间中的某些子问题根本没有必要求解,做备忘录方法有着只解那些肯定要求解的子问题的优点。
以上内容摘自机械工业出版社《算法导论》第15章:动态规划。为了便于理解,省略了具体的举例和复杂的推导公式等内容。如果您有空,推荐多花点时间去认真阅读原书。
相关阅读:
Python中的@cache有什么妙用?
📢欢迎 点赞👍 收藏⭐ 评论📝 关注❤ 如有错误敬请指正!
☟ 学Python,点击下方名片关注我。☟