目录
一、动态规划是什么?
动态规划(Dynamic Programming,简称 DP),听名字似乎有点高大上,让人摸不着头脑,但其实它的核心思想并不复杂。简单来说,动态规划是一种将复杂问题分解成一系列相对简单的子问题,并通过求解子问题来得到原问题最优解的方法。
为了让大家更好地理解,我们来举个生活中的例子。假设你计划去旅游,有多个城市可供选择,每个城市之间的交通费用不同,你希望规划出一条从出发地到目的地,且总费用最低的路线。这时候,动态规划就可以派上用场啦!
我们把整个旅程看作一个大问题,将其分解为多个阶段,每个阶段就是从一个城市到下一个城市的选择。比如,从城市 A 出发,你可以选择去城市 B、C 或 D,而从 B、C、D 又分别有不同的后续城市可以选择。我们可以通过计算每个阶段的最优选择,逐步构建出整个旅程的最优路线。
具体来说,在第一个阶段,你计算从 A 到 B、C、D 的费用,选择费用最低的路线作为当前的最优选择。到了第二个阶段,基于第一个阶段的最优选择,继续计算后续路线的费用,不断重复这个过程,直到到达目的地。在这个过程中,你会发现,有些子问题会被重复计算,比如从城市 B 到城市 E 的费用,在不同的路线选择中可能都会涉及到。动态规划的一个重要特点就是,它会记录已经计算过的子问题的解,当再次遇到相同的子问题时,直接使用之前的结果,而不是重新计算,这样就大大提高了计算效率 。
通过这个旅行规划的例子,你是不是对动态规划有了初步的认识呢?简单总结一下,动态规划就是把一个大问题拆分成小问题,通过解决小问题,找到大问题的最优解,同时避免重复计算,节省时间和精力。接下来,我们深入探讨动态规划的特点和应用场景。
二、动态规划的核心原理
了解了动态规划的基本概念后,我们深入探讨一下它的核心原理,主要包括最优子结构、无后效性和重叠子问题这三个关键特性 。掌握这些原理,能帮助你更好地理解动态规划算法的本质,在解决实际问题时更加得心应手。
2.1 最优子结构
最优子结构是动态规划的一个重要特性,它指的是问题的最优解包含了子问题的最优解 。简单来说,如果我们要求解一个大问题的最优解,那么可以通过求解它的子问题的最优解,然后将这些子问题的最优解组合起来,得到原问题的最优解。
以经典的背包问题为例,假设你有一个背包,它的容量为 5 千克,你有 3 个物品,分别是重量为 2 千克、价值为 3 元的物品 A,重量为 3 千克、价值为 4 元的物品 B,以及重量为 1 千克、价值为 2 元的物品 C。你需要在不超过背包容量的前提下,选择物品放入背包,使得背包中物品的总价值最大。
我们可以将这个问题分解为多个子问题。比如,先考虑只有物品 A 时,在背包容量为 1 千克、2 千克、3 千克、4 千克、5 千克的情况下,能获得的最大价值分别是多少;接着加入物品 B,计算在不同背包容量下,包含物品 A 和物品 B 时能获得的最大价值;最后加入物品 C,再次计算不同背包容量下的最大价值。
在这个过程中,我们发现,计算包含物品 A 和物品 B 时的最大价值,是基于只有物品 A 时的最大价值这个子问题的最优解。例如,当背包容量为 5 千克时,包含物品 A 和物品 B 的最大价值,可能是在只有物品 A 时,背包容量为 2 千克的最大价值(即放入物品 A,价值为 3 元),再加上物品 B 的价值(4 元),也可能是只有物品 A 时,背包容量为 5 千克的最大价值(即只放入物品 A,价值为 3 元),我们取这两者中的较大值,就是包含物品 A 和物品 B 时,背包容量为 5 千克的最大价值。这就是通过子问题的最优解来构建全局最优解的过程,体现了最优子结构的特性。
2.2 无后效性
无后效性是动态规划的另一个关键特性。它是指某个状态的未来发展只取决于当前状态,而与它是如何到达当前状态的过去决策过程无关 。也就是说,一旦当前状态确定,那么后续的决策和计算就只基于这个状态进行,不会受到之前决策的影响。
继续以上述背包问题为例,当我们确定了当前背包中已经放入了物品 A 和物品 B,背包剩余容量为 0 千克时,此时这个状态下的最大价值就是已经确定的,不会因为之前是先放入物品 A 还是先放入物品 B 而改变。后续如果还有新的物品要考虑放入,也是基于当前这个 “背包中有物品 A 和物品 B,剩余容量为 0 千克” 的状态来进行决策,而不会去考虑之前的放入顺序等历史信息。这就是无后效性的体现,它保证了我们在求解子问题时,可以按照一定的顺序进行,而不用担心之前的决策会对后续的计算产生干扰。
2.3 重叠子问题
重叠子问题是动态规划能够提高效率的重要基础。它是指在求解问题的过程中,会出现一些相同的子问题被多次计算的情况 。如果每次遇到这些子问题都重新计算,会浪费大量的时间和计算资源。动态规划通过记忆化存储的方式,将已经计算过的子问题的解保存下来,当再次遇到相同的子问题时,直接从存储中读取结果,而不需要重新计算,从而大大提高了算法的效率。
还是以背包问题来说,在计算不同背包容量和不同物品组合下的最大价值时,可能会多次遇到计算背包容量为 3 千克,包含物品 A 和物品 C 时的最大价值这个子问题。如果没有重叠子问题的概念,每次遇到都要重新计算一遍这个子问题,而利用动态规划,我们在第一次计算出这个子问题的解后,将其存储起来,后续再遇到同样的情况,直接读取存储的结果就可以了。这样,通过避免重复计算,大大加快了整个问题的求解速度。
三、动态规划解题步骤
了解了动态规划的核心原理后,我们来看看如何运用动态规划解决实际问题,一般来说,动态规划的解题步骤可以分为以下几步 。
3.1 定义状态
定义状态是动态规划解题的第一步,也是非常关键的一步。它的本质是找到一种合适的方式来描述问题的子问题,使得我们能够通过求解这些子问题,最终得到原问题的解。
在定义状态时,我们需要思考哪些信息能够完整地描述问题在某个阶段的情况,这些信息就是我们用来定义状态的变量。
以经典的斐波那契数列问题为例,斐波那契数列的定义是:\(F(0)=0\),\(F(1)=1\),\(F(n)=F(n - 1)+F(n - 2)\)(\(n\geq2\)) 。我们可以定义状态 \(dp[i]\) 表示第 \(i\) 个斐波那契数,这样就将求解整个斐波那契数列的问题,转化为求解每个 \(dp[i]\) 的子问题。
再比如背包问题,假设我们有一个容量为 \(W\) 的背包和 \(n\) 个物品,每个物品有重量 \(w_i\) 和价值 \(v_i\) 。我们可以定义状态 \(dp[i][j]\) 表示在前 \(i\) 个物品中选择,背包容量为 \(j\) 时能获得的最大价值。这里的 \(i\) 和 \(j\) 就是状态变量,它们完整地描述了问题在某个阶段的情况,即考虑到第 \(i\) 个物品,背包容量为 \(j\) 时的最大价值。