动态规划
题目来自
cyc2018 精选的力扣题
动态规划中包含3个重要的概念:
- 最优子结构
- 边界
- 状态转移公式。以爬楼梯为例,最优子结构为 \(f(10) = f(9) + f(8)\),边界是 \(f(1) = 1, f(2) = 2\),状态转移公式 \(f(n) = f(n-1) + f(n-2)\)
斐波那契数列
70. 爬楼梯
本质上为斐波那契数列。递归会超时(python 可设置缓存,不会超时),要用动态规划或直接应用通项公式
定义一个数组 \(dp\) 存储上楼梯的方法数(为了方便讨论,数组下标从 \(1\) 开始),\(dp[i]\) 表示走到第 \(i\) 个台阶的方法数目。
第 \(i\) 个台阶可以从第 \(i - 1\) 和 \(i - 2\) 个台阶再走一次到达,走到第 \(i\) 个台阶的方法数为走到第 \(i - 1\) 和第 \(i - 2\) 个楼梯的方法数之和。
198. 打家劫舍
定义 \(dp\) 数组用来存储最大的抢劫量,其中 \(dp[i]\) 表示抢到第 \(i\) 个住户时的最大抢劫量。
由于不能抢劫邻近住户,如果抢劫了第 \(i - 1\) 个住户,那么就不能再抢劫第 \(i\) 个住户,所以:\(dp[i] = \max (dp[i - 2]+nums[i], dp[i - 1])\)
衍生题:环形街区
取 \([1, n]\) 和 \([0, n - 1]\) 两者的最大值
矩阵路径
64. 最小路径和
62. 不同路径
排列组合
多重集排列问题(代码上是这个思路)
路径方向为多重集,有 向右 和 向下 两种(方向)元素,两种元素的重数分别为 \(n_{1}\) 和 \(n_{2}\),有 \(n = n_{1} + n_{2}\),则排列数为 \(\left(\begin{array}{l}{n} \cr {n_{1}} & {n_{2}}\end{array}\right) = \frac{n!}{n_{1}!n_{2}!}\)
⭐动态规划
令 \(dp[i][j]\) 是到达 \(i, j\) 的路径数,有 \(dp[i][j] = dp[i-1][j] + dp[i][j-1]\)
数组区间
303. 区域和检索 - 数组不可变
求数组区间 \((i, j)\) 的和
题中强调:会多次调用 sumRange 方法
因此预先求出所有 \((0, k)\) 的和,再 sum[j] - sum[i - 1]
413. 等差数列划分
等差数列满足:至少有 3 个元素、任意两个相邻(隔开不算)元素之差相同
暴力
每一对元素(之间至少隔着一个元素),根据两个元素之间的所有元素差值是否相等来判断是不是等差数列
💣动态规划
\(dp[i]\) 表示以 \(A[i]\) 为结尾(不是总的)的等差递增子区间的个数,对于 \(A = [0, 1, 2, 3, 4]\),有以下结论:
dp[2] = 1
[0, 1, 2]
dp[3] = dp[2] + 1 = 2
[0, 1, 2, 3], // [0, 1, 2] 之后加一个 3
[1, 2, 3] // 新的递增子区间
dp[4] = dp[3] + 1 = 3
[0, 1, 2, 3, 4], // [0, 1, 2, 3] 之后加一个 4
[1, 2, 3, 4], // [1, 2, 3] 之后加一个 4
[2, 3, 4] // 新的递增子区间
综上,在 A[i] - A[i-1] == A[i-1] - A[i-2] 时,dp[i] = dp[i-1] + 1
这里的 \(dp[i]\) 不是最终的结果,而是每步的结果。思考的时候过于死板
⭐分割整数
343. 整数拆分
没思路
贪心思想(举例得出规律)
数字 \(n\) 可由 \(a\) 个 \(x\) 和 \(1\) 个 \(b\) 相加而成。是否有优先级最高的因子 \(x\) 存在,有以下判断:
\(2 = 1 + 1\),\(1 * 1 < 2\),因此 \(2\) 比 \(1 + 1\) 更优;
\(3 = 1 + 2\),\(1 * 2 < 3\),因此 \(3\) 比 \(1\) 和 \(2\) 更优;
\(4 = 2 + 2\),\(2 * 2 = 4\),因此可以认为 \(4\) 与 \(2\) 等价,因此见到 \(4\) 就拆分;
\(5 = 2 + 3\);因为每个 \(5\) 都可以拆分为 \(2+3\),而 \(2 * 3 = 6 > 5\),因此见到 \(5\) 就拆分。
\(6 = 3 + 3 = 2 + 2 + 2\);因为 \(3 * 3 > 2 * 2 * 2 > 6\)。因此见到 \(6\) 就拆分,并且 \(3\) 是比 \(2\) 更优的因子。
易推出: 大数字都可以被拆分为多个小因子,以获取更大的乘积,只有 \(2\) 和 \(3\) 不需要拆分。 列出以下贪心法则:
- 第一优先级: \(3\);把数字 \(n\) 拆成尽可能多的 \(3\) 之和;
- 特殊情况: 拆完后,如果余数是 \(1\);则应把最后的 \(3 + 1\) 替换为 \(2 + 2\),因为后者乘积更大;
- 第二优先级: \(2\);留下的余数如果是 \(2\),则保留,不再拆为 \(1+1\)
当 \(n <= 3\) 时,直接返回 \(n - 1\)
动态规划
\(dp[i]\) 表示:数字 \(i\) 拆分为至少两个正整数之和的最大乘积。
有转移方程:\(dp[i] = \max (dp[i], j * dp[i - j])\)
由于 \(i - j <= 3\) 时,\(dp[i - j] = i - j - 1 < i - j\),所以 \(dp[i] = \max (dp[i], j * \max(dp[i - j], i - j))\)
279. 完全平方数
BFS
前面有做过
动态规划
\(dp[i]\) 表示:数字 \(i\) 拆分为完全平方数的最少个数。
有转移方程:\(dp[i] = \min (dp[i], dp[i - square] + 1), square = 1, 4, 9... <= i\)
💣91. 解码方法
字符串中可能包含 "0",因此情况比较复杂
- 若
s[i] == "0"- 若
s[i - 1] = "1" or "2",则 \(dp[i] = dp[i - 2]\) - 否则,
return 0
- 若
- 若
s[i] != "0"- 若
s[i - 1] == "1",则 \(dp[i] = dp[i - 1] + dp[i - 2]\) - 若
s[i - 1] == "2" and "1" <= s[i] <= "6",则 \(dp[i] = dp[i-1] + dp[i-2]\)- 解释:
s[i - 1]与s[i]分开译码,为 \(dp[i - 1]\);合并译码,为 \(dp[i - 2]\)
- 解释:
- 否则,\(dp[i] = dp[i - 1]\)
- 解释:此时若合并译码,则大于 \(26\),
s[i - 1]与s[i]只能分开译码
- 解释:此时若合并译码,则大于 \(26\),
- 若
最长递增子序列
300. 最长上升子序列
动态规划
\(dp[i]\) 表示以 \(S_i\) 结尾的序列的最长递增子序列长度。对于每个 \(i\),向前遍历以寻找递增子序列
⭐376. 摆动序列
动态规划(想不到)
用两个 \(dp[i]\) 数组。\(up[i]\) 表示前 \(i\) 个元素中摆动序列以上升元素结尾的最长子序列长度;\(down[i]\) 反之。
若第 \(i\) 个元素上升就更新 \(up[i]\),如下代码:(\(down[i]\) 同理)
if (nums[i] > nums[j]) {
up[i] = Math.max(up[i], down[j] + 1);
}
贪心算法
最长公共子序列
二维数组 \(dp\) 用来存储最长公共子序列的长度,其中 \(dp[i][j]\) 表示 \(S1\) 的前 \(i\) 个字符与 \(S2\) 的前 \(j\) 个字符最长公共子序列的长度,状态转移方程:
\(d p[i][j]=\left\lbrace\begin{array}{ll} dp[i-1][j-1]+1 && S1_{i} == S2_{j} \cr \max (dp[i-1][j], dp[i][j-1]) && S1_{i} != S2_{j} \end{array}\right.\)
特别需要注意的是,当 \(S1_{i} != S2_{j}\) 时,\(dp[i][j] != dp[i-1][j-1]\)。比如:
abcd和adbc当 \(i\) 和 \(j\) 为 \(4\) 时,若用错误的表达式,则最长公共子序列为 \(2\)
⭐221. 最大正方形
这个转移方程想不到
转移方程:\({dp}(i, j)=\min ({dp}(i-1, j), {dp}(i-1, j-1), {dp}(i, j-1))+1\)
若某格子值为 1 ,则以此为右下角的正方形的、最大边长为:上面的正方形、左面的正方形或左上的正方形中,最小的那个,再加上此格。

⭐🌟0 - 1 背包(难)
此类问题的特点:一般都有选或不选两种选择
不能使用贪心算法
什么是 0 - 1 背包问题?
- 有 \(n\) 件物品,每件物品的重量为 \(w[i]\),价值为 \(c[i]\)。现有一个容量为 \(V\) 的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都只有 1 件
令 \(dp[i][v]\) 表示前 \(i\) 件物品 \((1 ≤ i ≤ n, 0 ≤ v ≤ V)\) 恰好装入容量为 \(V\) 的背包中所能获得的最大价值。① 第 \(i\) 件物品不放入 ② 第 \(i\) 件物品放入
状态转移方程:\({dp}[{i}][{v}]=\max ({dp}[{i}-1][{v}], {dp}[{i}-1][{v}-{w}[{i}]]+{c}[{i}])\)
\(\quad(1 ≤ {i} ≤ {n}, {w}[{i}] ≤ {v} ≤ {V})\)
空间优化后:(从大到小枚举)
\({dp}[{v}]=\max ({dp}[{v}], {dp}[{v}-{w}[{i}]]+{c}[{i}])\)
\(\quad(1 ≤ {i} ≤ {n}, {V} ≥ {v} ≥ {w}[{i}])\)
兄弟问题:完全背包问题(从小到大枚举)
- 有 \(n\) 种物品,每种物品的单件重量为 \(w[i]\),价值为 \(c[i]\)。现有一个容量为 \(v\) 的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都有无穷件
空间优化后:(从小到大枚举)
\({dp}[{v}]=\max ({dp}[{v}], {dp}[{v}-{w}[{i}]]+{c}[{i}])\)
\(\quad(1 ≤ {i} ≤ {n}, {w}[{i}] ≤ {v} ≤ {V})\)
子问题:多维费用的 0-1 背包问题
416. 分割等和子集
动态规划
\(dp[i][j]\) 表示从数组的 \([1, i]\) 这个子区间内挑选一些正整数,每个数只能用一次,是否存在数的和恰好等于 \(j\) 的情况(boolean 类型)
DFS
494. 目标和
01 背包其实不是这种解法的重点,重点是怎么把题目转化成求解 01 背包的形式
本题的 DFS 无法剪枝,为暴力解法
动态规划
思路正常版
\(dp[i][j]\) 表示用数组中的前 \(i\) 个元素,组成和为 \(j\) 的方案数:\(dp[i][j] = dp[i - 1][j - nums[i]] + dp[i - 1][j + nums[i]]\)
递推形式:
\(dp[i][j + nums[i]] += dp[i - 1][j]\)
\(dp[i][j - nums[i]] += dp[i - 1][j]\)
由于数组中所有数的和不超过 \(1000\),那么 \(j\) 的最小值可以达到 \(-1000\)。在很多语言中,是不允许数组的下标为负数的,因此我们需要给 \(dp[i][j]\) 的第二维预先增加 \(1000\)
巧妙版
转换为子集和
https://leetcode-cn.com/problems/target-sum/solution/c-dfshe-01bei-bao-by-bao-bao-ke-guai-liao/

⭐474. 一和零
多维费用的 0-1 背包问题(此题为二维)
本题有 0 字符串和 1 字符串两种背包,每个字符串的 0 1 个数为费用(体积),每个字符串的价值设为 1
\(dp(i, j)\) 表示使用 \(i\) 个 \(0\) 和 \(j\) 个 \(1\) 最多能拼出的字符串数目。
转移方程(空间优化):
\(dp[i][j] = \max (dp[i][j], 1 + dp[i - zeroNums][j - oneNums])\)
因为要用到前一个字符串的状态,所以费用 i j 要倒序遍历
322. 零钱兑换
完全背包问题(正序)
总金额 amount 为背包容量,单个硬币的面额为物品单件的重量,物品的价值在此题未用到
139. 单词拆分
因为单词可以被用多次,所以是完全背包问题。
求解顺序的完全背包问题时,对物品的迭代应该放在最里层,对背包的迭代放在外层,只有这样才能让物品按一定顺序放入背包中
因为单词的顺序是严格的,所以遍历单词表必须放在内循环
\(dp[i]\) 表示字符串 s 的前 \(i\) 长度的子串能否被拆分
39. 组合总和
仍然是完全背包问题
它的三个兄弟问题在 搜索 - 回溯 专题
股票交易
⭐309. 最佳买卖股票时机含冷冻期
允许多次买卖,但有冷冻期
状态定义
\(dp[i][j]\) 表示 \([0, i]\) 区间内,到第 \(i\) 天(从 \(0\) 开始)状态为 \(j\) 时的最大收益
这里 \(j\) 取三个值:
0 表示不持股;1 表示持股;2 表示处在冷冻期
状态转移方程
- 不持股可以由这两个状态转换而来:(1)昨天不持股,今天什么都不操作,仍然不持股。(2)昨天持股,今天卖了一股
- 持股可以由这两个状态转换而来:(1)昨天持股,今天什么都不操作,仍然持股;(2)昨天处在冷冻期,今天买了一股
- 处在冷冻期只可以由不持股转换而来(冷冻期其实也算不持股)
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][2] - prices[i]);
dp[i][2] = dp[i - 1][0];
初值
持股的初值为 -price[0](想象成花了这么多钱买入)
输出
返回最后一天不持股的值
714. 买卖股票的最佳时机含手续费
允许多次买卖,但有手续费
状态定义
\(dp[i][j]\) 表示 \([0, i]\) 区间内,到第 \(i\) 天(从 \(0\) 开始)状态为 \(j\) 时的最大收益
这里 \(j\) 取两个值:
0 表示不持股;1 表示持股
状态转移方程
- 不持股可以由这两个状态转换而来:(1)昨天不持股,今天什么都不操作,仍然不持股。(2)昨天持股,今天卖了一股(要减去手续费)
- 持股可以由这两个状态转换而来:(1)昨天持股,今天什么都不操作,仍然持股;(2)昨天不持股,今天买了一股
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
初值和输出
与上题(冷冻期)相同
💣买卖股票的最佳时机 III & IV
限定交易次数为 n 次
状态定义
\(dp[i][j][k]\) 表示 \([0, i]\) 区间内,到第 \(i\) 天已经进行了第 \(j\) 次买入,且交易状态为 \(k\) 时的最大收益
这里 \(j\) 取 n 个值:表示最多可进行 n 次交易
\(k\) 取三个值:
0 表示不持股;1 表示持股
状态转移方程
- 不持股可以由这两个状态转换而来:(1)之前已经进行了 j 次买卖,昨天不持股,今天什么都不操作,仍然不持股。(2)昨天持股,并且是第 j 次买卖,今天卖了股票
- 持股可以由这两个状态转换而来:(1)昨天持股,今天什么都不操作,仍然持股;(2)已经进行了 j - 1 次买卖,昨天不持股,今天买了一股,进行第 j 次持股
状态转移方程照着之前的题目修改会更快想出来
dp[i][j][0] = Math.max(dp[i - 1][j][0],dp[i - 1][j][1] + prices[i]);
if (j == 0) {
dp[i][j][1] = Math.max(dp[i - 1][j][1], -prices[i]);
} else {
dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
}
初值(这题的初值很重要)
所有不持股的状态值初始化的时候为 0,所有持股的状态值都设置为一个很大的负数或 -price[0]
持股状态的初值一开始只给第一次交易赋值了(第二次、第三次……都为 0),这样在对 dp[i][j][1] 的赋值时,会出问题
for (int i = 0; i < dealNums; i++) {
dp[0][i][1] = -prices[0];
}
输出
返回最后一天、最后一次不持股的值
字符串编辑
583. 两个字符串的删除操作
本质为求最长公共子序列
650. 只有两个键的键盘
有一个性质:若两个数有 n 倍关系,那么翻 n 倍,就加 n 步
比如 3 需要 3 步,那么 15 就要 3 + 5 = 8 步
72. 编辑距离(困难)
思路和最长公共子序列其实很相似,但是更难理解
状态定义
\(dp[i][j]\) 代表 word1 到 \(i\) 位置转换成 word2 到 \(j\) 位置需要最少步数
状态转移方程
当 word1[i] == word2[j],\(dp[i][j] = dp[i-1][j-1]\)
当 word1[i] != word2[j],\(dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1\)
其中,\(dp[i-1][j-1]\) 表示替换操作,\(dp[i-1][j]\) 表示删除操作,\(dp[i][j-1]\) 表示插入操作,解释如下:
- $dp[i-1][j-1]$,即先将 word1 的前 4 个字符 hors 转换为 word2 的前 2 个字符 ro,然后将第五个字符 word1[4](因为下标基数以 0 开始) 由 e 替换为 s(即替换为 word2 的第三个字符,word2[2])
- $dp[i][j-1]$,即先将 word1 的前 5 个字符 horse 转换为 word2 的前 2 个字符 ro,然后在末尾补充一个 s,即插入操作
- $dp[i-1][j]$,即先将 word1 的前 4 个字符 hors 转换为 word2 的前 3 个字符 ros,然后删除 word1 的第 5 个字符
初值
注意,针对第一行,第一列要单独考虑,我们引入 '' 下图所示:

第一行,是 word1 = '' 为空变成 word2 最少步数,就是插入操作
第一列,是变为 word2='' 为空,需要的最少步数,就是删除操作

浙公网安备 33010602011771号