动态规划问题总结
基本原理
动态规划核心是穷举求最值,动态规划三要素:
- 最优子结构:
是否
—— 能够通过子问题
的最值得到原问题
的最值。- 子问题间必须互相独立,不能有制约关系。
- 状态转移方程:
如何
—— 通过子问题
的最值得到原问题
的最值。也就是暴力解。 - 重叠子问题:使用「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
简而言之,如果一个问题要求最值
,而且可以分解成子问题
,就可以尝试写状态转移方程,写出来就可以看出来有没有重叠子结构
了(递归树中有没有重复的节点),就可以再考虑是否可以动态规划解决。
如何分析一个动态规划问题?
- 明确「Base Case」:最简单的情况,基本情况。
- 明确「状态」:原问题和子问题中变化的量。
- 明确「选择」:可以导致状态产生改变的行为。
- 明确「dp 数组/函数」的含义:参数/索引一般是状态,返回值是要求的值。但是有时要灵活变通使得通项公式好写。几个可以考虑的方向:
- 在暴力求解中路径到
以该状态为止
的情况的最值。
- 在暴力求解中路径到
- 写状态转移方程,经常不止会用到前一个状态,而是会用到所有过去的状态。
注:dp
数组的本质是cache,有时候index不是连续的(与所给input的值而非数量有关),这时候需要考虑用hashing来存储中间结果。
# 自顶向下递归的动态规划
def dp(状态1, 状态2, ...):
for 选择 in 所有可能的选择:
# 此时的状态已经因为做了选择而改变
result = 求最值(result, dp(状态1, 状态2, ...))
return result
# 自底向上迭代的动态规划
# 初始化 base case
dp[0][0][...] = base case
# 进行状态转移
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2...)
各种类型
子序列,字符串
这类型的DP数组一般有两种类型:
- 一维数组:
- 子数组
arr[0..i]
中,我们要求的值是dp[i]
。 - 子数组
arr[0..i]
中,以arr[i]状态结尾
的情况的最值是dp[i]
。
- 子数组
- 二维数组:
- 一般是涉及两个数组,或者两个字符串的:在子数组
arr1[0..i]
和子数组arr2[0..j]
中,我们要求的子序列长度为dp[i][j]
:经典案例:Edit Distance - 在子数组
array[i..j]
中,我们要求的子序列的长度为dp[i][j]
。
- 一般是涉及两个数组,或者两个字符串的:在子数组