动态规划是解决问题的一种思想,而不是一个具体算法。此类问题的重点是:
- 状态划分
- 状态转移方程
常见的动态规划类型
- 线性 DP:状态的排布是线性的,例如最长单调上升子序列
- 区间 DP:在区间上 DP,是线性DP的扩展。
f[i][j]
可以由[i, j]
的子区间的最优解更新得到 - 树状 DP:状态的排布是树状的。例如数塔
- 背包 DP:背包体积有限,每个物品有价值,求能装下的最大价值
- 数位 DP:区间里有多少数字包含某个性质
- 状态压缩 DP:把状态用二进制表示
常见的动态规划优化
- 滚动数组优化:节省空间
- 矩阵乘法优化:
- 斜率优化:
- 四边形不等式优化:
- 决策单调性优化:
- 数据结构优化:优化时空
能用动态规划解决的问题类型
并不是所有问题都能用动态规划。要使用动态规划,需要满足三个条件:
- 能划分阶段
- 最优化原理:子问题的局部最优解会形成整个问题的全局最优解
- 无后效性原则:某个阶段的状态一旦确定,后续过程的演变不再受此前的状态及决策影响。过去与未来无关。
动态规划解题方法
动态规划问题都可以通过划分阶段、进行决策得到一个递推方程。然后根据这个方程就可以写代码了。
写代码的时候要注意边界条件和递归结束条件。
- 正推(递推):从初始状态开始,逐步决策,直到结束状态。例如阶乘递推式:
F[i + 1] = F[i] * i
- 倒推(记忆化搜索):从结束状态开始,倒着选择中间阶段的决策,到达开始状态。例如阶乘:
dp[i] = dp[i - 1] * i
概念
示例:数塔问题:
9
4 7
2 3 2
3 2 5 2
从数塔顶端的一个元素出发,每次可以向左下或右下移动,最终移动到最下层。求路径元素和的最大值。
阶段
把问题的全过程拆分成若干个相互联系的阶段,从而把一个问题变成多阶段决策问题。
对于上面的数塔,每一层就是一个阶段。
状态
每个阶段包含若干个状态。
对于上面的数塔,每一层就是一个阶段,每一层的每个元素就是一个状态。
决策
问题解决过程中,每个阶段做出的选择就是决策。
对于上面的数塔,每次都可以选择向左下或右下移动,但最终只能选一个方向移动,这个最终的选择的方向就是决策。
每个决策对应一个决策变量。实际问题中的决策变量往往是受限定的,这个限定范围就是决策允许集合。
策略、最优策略
所有阶段的一次排列构成问题的全过程。全过程中决策变量的有序总体就是策略,也就是实际问题的一个解。
对于上面的数塔,每一层选出一个数,最终到最下层,这个路径就是一个策略。其中路径上所有元素之和最大的那条就是最优策略。
状态转移方程
在相邻两个阶段之间转移时,可以用状态转移方程描述。对前一阶段的决策产生后一阶段的状态。
示例
示例一 数塔问题
9
4 7
2 3 2
3 2 5 2
从数塔顶端的一个元素出发,每次可以向左下或右下移动,最终移动到最下层。求路径元素和的最大值。
用 dp[i][j]
表示第 i 层的第 j 个元素对应的最大和。
- 阶段:每一层就是一个阶段
- 状态:每层的每个元素就是这一层的一个状态
- 决策:在每个位置,都需要判断左上方元素和右上方元素累计和的大小,并用大的作为自己的上一个元素
- 策略:第 i 层有 i 个状态可以选择,共有 i ! i! i! 个策略。
- 状态转移方程:
d[i][j] = max(dp[i-1][j-1], dp[i-1][j]) + arr[i][j]
示例二 最长单调上升子序列 TODO
问题:给定一个数组,求其最长单调增的子序列。例如对于 [1 2 6 3 5]
,其解为 [1 2 3 5]
。
用 dp[i][j]
表示目前已经分析了前 i 个元素,这个元素的最长单调上升子序列的最后一个元素是 j。对于上面例子,dp[0][1]
= 1,dp[1][2]
= 2,dp[2][5]
= 3,dp[3][3]
= 3,dp[3][5]
= 3,dp[4][4]
= 4。
- 阶段:每个元素对应一个阶段
- 状态:选或不选
- 决策:
- 策略:共 2 n 2^n 2n 个策略
- 状态转移方程:
dp[i][j] = max( f[i-1][j] | j <= a[i], )
示例三 最大和
从 n 个数中取出 k 个数,使其和最大。
用 dp[i][j]
表示当前已经判断到了第 i 个数(从 0 ~ i),并取出了 j 个。
- 阶段:每个数就是一个阶段
- 状态:选择或不选择
- 决策:判断
dp[i-1][j]
(不选当前元素)与dp[i-1][j-1] + arr[i]
(选择当前元素)的大小,若后者大则选择当前元素,否则不选 - 策略:每个元素都可以选则或不选,共 2 i 2^i 2i 个策略
- 状态转移方程:
dp[i][j] = max( dp[i-1][j], dp[i-1][j-1] + arr[i] )