循序渐进,搞懂什么是动态规划

news2024/11/17 9:52:42

循序渐进,搞懂什么是动态规划

写在前面

温馨提示,本文的篇幅很长,需要花很长的时间阅读。如果要完全理解所有内容,还需要花更多的时间学习。如果打算认真学习动态规划,又不能一次看完,建议您收藏本文以便后续回来看,作为学习的参考。

要学习和理解动态规划,不太可能通过碎片时间完全学会,需要花很多时间认真学习。本文的内容尽量避免了各种复杂的公式推导过程,重在理解动态规划的思想和方法。如果要精通动态规划,需要多结合实战。

动态规划是什么

动态规划(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. 描述最优解的结构。
  2. 递归定义最优解的值。
  3. 按自底向上的方式计算最优解的值。
  4. 由计算出的结果构造一个最优解。

第1~3步构成问题的动态规划解的基础。第4步在只要求计算最优解的值时可以略去。如果的确做了第4步,则有时要在第3步的计算中记录一些附加信息,使构造一个最优解变得容易。

动态规划基础

在本节中,我们要介绍适合采用动态规划方法的最优化问题中的两个要素:最优子结构和重叠子问题。另外,还要分析一种不同的方法,称为做备忘录(memoization),以充分利用重叠子问题性质。

最优子结构

用动态规划求解优化问题的第一步是描述最优解的结构。回顾一下,如果问题的一个最优解中包含了子问题的最优解,则该问题具有最优子结构。当一个问题具有最优子结构时,提示我们动态规划可能会使用。在动态规划中,我们利用子问题的最优解来构造问题的一个最优解。因此,必须小心以确保在我们所考虑的子问题范围中,包含了用于一个最优解中的哪些子问题。

在找寻最优子结构时,可以遵循一种共同的模式:
1)问题的一个解可以是做一个选择,做这种选择会得到一个或多个有待解决的子问题。
2)假设对一个给定的问题,已知的是一个可以导致最优解的选择。不必关心如何确定这个选择,尽管假定它是已知的。
3)在已知这个选择后,要确定哪些子问题会随之发生,以及如何最好地描述所得到的子问题空间。
4)利用一种“剪贴”(cut-and-paste)技术,来证明在问题的一个最优解中,使用的子问题的解本身也必须是最优的。通过假设每一个子问题的解都不是最优解,然后导出矛盾,即可做到这一点。特别地,通过“剪除”非最优的子问题解再“贴上”最优解,就证明了可以得到原问题的一个更好的解,因此,这与假设已经得到一个最优解相矛盾。如果有多于一个的子问题的话,由于它们通常非常类似,所以只要对其中一个子问题“剪贴”处理略加修改,即可很容易地用于其他子问题。
为了描述子问题空间,可以遵循这样一条有效的经验规则,就是尽量保持这个空间简单,然后在需要时再扩充它。

最优子结构在问题域中以两种方式变化:
1)有多少个子问题被使用在原问题的一个最优解中。
2)在决定一个最优解中使用哪些子问题时有多少个选择。

非正式地,一个动态规划算法的运行时间依赖于两个因素的乘积:子问题的总个数和每一个子问题中有多少种选择。

动态规划以自底向上的方式来利用最优子结构。也就是说,首先找到子问题的最优解,解决子问题,然后找到问题的一个最优解。寻找问题的一个最优解需要在子问题中做出选择,即选择将用哪一个来求解问题。问题解的代价通常是子问题的代价加上选择本身带来的开销。

重叠子问题

适用于动态规划求解的最优化问题必须具有的第二个要素是子问题的空间要“很小”,也就是用来解原问题的递归算法可反复地解同样的子问题,而不是总在产生新的子问题。典型地,不同的子问题数是输入规模的一个多项式。当一个递归算法不断地调用同一问题时,我们说该最优问题包含重叠子问题。相反地,适合用分治法解决的问题往往在递归的每一步都产生全新的问题。动态规划算法总是充分利用重叠子问题,即通过每个子问题只解一次,把解保存在一个在需要时就可以查看的表中,而每次查表的时间为常数。

动态规划要求其子问题既要独立又要重叠,这看上去似乎有些奇怪。虽然这两点要求听起来可能是矛盾的,但它们描述了两种不同的概念,而不是同一个问题的两个方面。如果同一问题的两个子问题不共享资源,则它们就是独立的。对两个子问题来说,如果它们确实是相同的子问题,只是作为不同问题的子问题出现的话,则它们是重叠的。

把自顶向下的递归算法与自底向上的动态规划算法做比较,可以看出后者更加有效,因为它利用了重叠子问题的性质。动态规划算法对每一个子问题只解一次,递归算法对在递归树中重复出现的每个子问题都要重复解一次。当某个问题的自然递归解的递归树中反复包含同一个子问题,而且不同的子问题个数很小,可以考虑能否用动态规划来解这个问题。

做备忘录

动态规划有一种变形,它即具有通常的动态规划方法的效率,又采用了一种自顶向下的策略。其思想就是备忘(memoize)原问题的自然但低效的递归算法。像在通常的动态规划中一样,维护一个记录子问题解的表,但有关填表动作的控制结构更像递归算法。

加了备忘的递归算法为每一个子问题的解在表中记录一个表项。开始时,每个表项最初都包含一个特殊的值,以表示该表项有待填入。当在递归算法的执行中第一次遇到一个子问题时,就计算它的解并填入表中。以后每次遇到该子问题时,只要查看并返回表中先前填入的值即可。

在实际应用中,如果所有的子问题都至少要被计算一次,则一个自底向上的动态规划算法通常要比一个自顶向下的做备忘录算法好出一个常数因子,因为前者无需递归的代价,而且维护表格的开销也小些。此外,在有些问题中,还可以用动态规划算法中的表存取模式来进一步减少时间或空间上的需求。或者,如果子问题空间中的某些子问题根本没有必要求解,做备忘录方法有着只解那些肯定要求解的子问题的优点。

以上内容摘自机械工业出版社《算法导论》第15章:动态规划。为了便于理解,省略了具体的举例和复杂的推导公式等内容。如果您有空,推荐多花点时间去认真阅读原书。


相关阅读:

Python中的@cache有什么妙用?

📢欢迎 点赞👍 收藏⭐ 评论📝 关注 如有错误敬请指正!

☟ 学Python,点击下方名片关注我。☟

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/634237.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《深入理解计算机系统》(6)存储器层次结构

1、存储技术 随机访问存储器,分为两类: RAM,同时也是易失性存储器,也分为两类: - SRAM:静态随机访问存储器,速度快,价格高。多用来作为高速缓存存储器。 - DRAM:动态随机…

WinDbg安装入坑1(C#)

由于作者水平有限,如有写得不对的地方,请指正。 使用WinDbg的过程中,坑特别的多,对版本要求比较严格,如: 1 32位应用程序导出的Dump文件要用32位的WinDbg打开,想要没有那么多的问题&#xff…

chatgpt赋能python:Python删除内容:掌握三种删除方式

Python删除内容:掌握三种删除方式 删除变量中的值 删除变量中的值是Python编程中常见的操作。Python提供了del语句用于删除变量中的值: x "Hello World" del x上述代码中,del x语句将删除变量x中的值。如果我们在执行print(x)时…

从C语言到C++_18(stack和queue的常用函数+相关练习)力扣

目录 1. stack 1.1 栈的概念 1.2 stack 的介绍和使用 2. queue 2.1 队列的概念 2.2 queue 的介绍和使用 3. 栈和队列的相关选择题 答案: 4. 栈和队列的相关OJ题 155. 最小栈 - 力扣(LeetCode) 解析代码: 剑指 Offer 3…

python学习-代码调试器

目录 为什么学习调试器Pycharm Debugger示例所用代码布局调试工具栏 Debug Bar程序控制工具栏 pdb查看源代码 l list查看当前函数源代码 ll longlist打印变量 p查看调用栈w where向上移动当前帧 u up向上移动当前帧 d down运行当前行代码,在第一个可以停止的位置停下 s step继续…

Selenium基础篇之八大元素定位方式

文章目录 前言一、如何进行元素定位?1.右击元素-检查2.F12-选择工具点击元素3.借助selenium IDE 二、八大元素定位方式1.ID1.1 方法1.2 举例1.3 代码1.4 截图 2.NAME2.1 方法2.2 举例2.3 代码2.4 截图 3.CLASS_NAME3.1 方法3.2 举例3.3 代码3.4 截图 4.TAG_NAME4.1 …

再也不用担心组件跨层级的数据共享和方法驱动了

文章目录 兄弟组件的传值和方法调用多个独立组件的数据共享和方法调用多个组件内的方法和数据互相驱动:eventBus多个组件的数据共享以及数据修改:vuex 项目中关于组件的使用经常会碰到这种情况:父子组件传和方法调用、兄弟组件的传值和方法调…

Selenium该如何实现微博自动化运营:关注、点赞、评论

目录 前言: Selenium 是什么? 一、核心代码 二、步骤分解 三、自动化运营常用工具 前言: 使用 Selenium 实现微博自动化运营,可以提高效率、减少工作量,下面讲解如何使用 Selenium 实现微博的关注、点赞和评论功…

webSocket 学习

引子 WebSocket 是一种在单个 TCP 连接上进行全双工通信的网络协议。它是 HTML5 中的一种新特性,能够实现 Web 应用程序和服务器之间的实时通信,比如在线聊天、游戏、数据可视化等。 相较于 HTTP 协议的请求-响应模式,使用 WebSocket 可以建…

JVM零基础到高级实战之Java内存区域虚拟机栈

JVM零基础到高级实战之Java内存区域虚拟机栈 JVM零基础到高级实战之Java内存区域虚拟机栈 文章目录 JVM零基础到高级实战之Java内存区域虚拟机栈前言JVM内存模型之虚拟机栈总结 前言 JVM零基础到高级实战之Java内存区域虚拟机栈 JVM内存模型之虚拟机栈 虚拟机栈是什么&#x…

Ansys Lumerical | 光纤布拉格光栅温度传感器的仿真模拟

说明 该示例演示了一种基于光纤布拉格光栅(FBG)的温度传感器,因为光纤折射率会随温度而变化,导致其布拉格波长发生偏移,所以可以被用作温度的测量。(联系我们获取文章附件) 综述 在本示例中要考…

java八股文-并发篇

并发篇 1. 线程状态 要求 掌握 Java 线程六种状态掌握 Java 线程状态转换能理解五种状态与六种状态两种说法的区别 六种状态及转换 分别是 新建 当一个线程对象被创建,但还未调用 start 方法时处于新建状态此时未与操作系统底层线程关联 可运行 调用了 start …

在Linux上安装Zookeeper集群(zookeeper-3.5.9)

记录:455 场景:在CentOS 7.9操作系统上,使用zookeeper-3.5.9版本,在三台机器上,安装Zookeeper集群。 版本:zookeeper-3.5.9,CentOS 7.9,Linux kernel-5.4.218。 1.主机规划 目标&#xff1a…

接口自动化测试新玩法!Python构建mock服务让你的测试更加高效!

目录 引言 Flask mock接口开发示例 引言 Mock 即模拟,就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法,其最大的优势就是降级前后端耦合度, 使前端工程师可以不…

Alloy Tutorial(1)Alloy 基本使用

文章目录 构造一个 graph谓词undirected 无向图undirected2 无向图的第二种写法assertFact扩展 构造一个 graph In this workshop we are going to use Alloy to model graphs. Mathematically, recall that a graph is just a pair ⟨V, E⟩ where V is a set of vertices (a…

参数估计和假设检验的区别与联系

1.参数估计和假设检验的区别与联系 笔记来源: 参数估计与假设检验 参数估计和假设检验有什么区别? 1.1 联系 参数估计和假设检验是推断统计的两个组成部分,它们都是根据样本信息对总体的数量特征进行推断 下图来自《统计学图鉴》 参数估计…

详解WEB集群服务(LNMP+Nginx+Tomcat+Rewrite重写+七层反向代理+SNAT|DNAT策略)

实战项目演练 1.问题描述2.实验操作步骤2.1 CentOS 7-1客户端配置2.2 CentOS 7-2网关服务器配置2.3 CentOS 7-8 (Web1:Tomcat服务器)2.3.1 安装Tomcat服务器2.3.2 提供四层反向代理的动态页面 2.4 CentOS 7-9 (Nginx服务器)2.4.1 安装Nginx服务2.4.2 安装MySQL服务2.4.3 安装配…

基于Python的接口自动化-读写配置文件

目录 引言 configparser模块功能介绍 引言 在编写接口自动化测试脚本时,有时我们需要在代码中定义变量并给变量固定的赋值。为了统一管理和操作这些固定的变量,咱们一般会将这些固定的变量以一定规则配置到指定的配置文件中,后续需要用到这…

如何在电脑上使用wink一键高清短视频

如何在电脑上使用wink一键高清优化短视频画质 文章目录 如何在电脑上使用wink一键高清优化短视频画质1.软件简介1.1痛点1.2解决方案 2.实际操作2.1准备工作2.1.1下载雷电模拟器2.1.2下载wink 2.2.安装软件2.2.1安装雷电模拟器2.2.2安装wink2.2.2.1在雷电模拟器中安装wink2.2.2.…

LeetCode 双周赛 106(2023/06/10)两道思维题

本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 加入知识星球提问。 往期回顾:LeetCode 单周赛第 348 场 数位 DP 模版学会了吗? 双周赛 106 概览 T1. 判断一个数是否迷人(Easy) 标…