动态规划
这是一篇关于 leetcode 动态规划的笔记。
思路
对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!
-
确定dp数组(dp table)以及下标的含义
-
确定递推公式
-
dp数组如何初始化
-
确定遍历顺序
-
举例推导dp数组
70. 爬楼梯

思路分析:倒推法
想象你站在第 阶台阶上,你是怎么上来的?
-
你可能从第 阶跳了 1 步上来的。
-
你也可能从第 阶跳了 2 步上来的。
所以,到达第 阶的方法总数,就等于到达 阶的方法数加上到达 阶的方法数。
这就是著名的状态转移方程:
解法一:动态规划(空间优化版)
虽然我们可以开一个数组来存所有的值,但由于 只依赖于前两个数,我们只需要两个变量来滚动记录即可。这种做法的时间复杂度是 ,空间复杂度是 。
1 | class Solution: |
118. 杨辉三角

动态规划思路
我们可以把杨辉三角看作一个二维数组(或者是嵌套列表)。
-
状态定义:设 为第 行第 列的数字。
-
转移方程:
-
对于非首尾元素:
-
-
边界条件:
-
每一行的第一个元素和最后一个元素都是 1:
-
1 | class Solution: |
198. 打家劫舍

动态规划思路:状态转移
我们可以通过构建一个递推关系来解决:
-
定义状态:
-
设 为偷到第 间房屋时,能够获得的最高金额。
-
状态转移方程:
-
当我们走到第 间房(金额为 )时,我们有两种选择:
-
选择偷:那么我们就不能偷第 间房,所以总金额 = 第 间房的最优解 + 当前房间金额:。
-
选择不偷:那么我们可以维持偷到第 间房时的最优解:。
-
-
为了钱更多,我们取两者的最大值:
-
1 | class Solution: |
279. 完全平方数

我们要寻找的是:和为 的完全平方数的最少数量。
① 确定 dp 数组(dp table)以及下标的含义
-
dp[i]:和为 的完全平方数的最少数量。 -
我们的目标是计算出
dp[n]。
② 确定递推公式
对于每一个数 ,我们可以尝试减去一个完全平方数 (其中 )。
如果我们减去了 ,那么剩下的数就是 。此时所需的平方数数量就是 dp[i - j*j] + 1。
为了找到最少数量,我们需要遍历所有可能的 :
其中 。
③ dp 数组如何初始化
-
dp[0] = 0:和为 0 的最少平方数数量显然是 0。 -
非零下标初始化:因为我们要取的是最小值(
min),所以非零位置应初始化为一个极大值。在 Python 中可以用float('inf')或者 (因为最坏情况全是 1,数量不会超过 )。
④ 确定遍历顺序
-
由于
dp[i]取决于比它小的状态dp[i - j^2],所以必须从前向后遍历。 -
外层循环:遍历数字 (从 1 到 )。
-
内层循环:遍历可能的完全平方数 。
⑤ 举例推导 dp 数组
以 为例:
-
dp[0] = 0 -
i = 1: 只能减去 。 -
i = 2: 只能减去 。 -
i = 3: 只能减去 。 -
i = 4:-
减去 :
-
减去 :
-
取最小值:
dp[4] = 1
-
1 | import math |
322. 零钱兑换

假设输入为 coins = [1, 2, 5], amount = 11。
① 确定 dp 数组(dp table)以及下标的含义
dp[j]:凑足总额为j所需的最少硬币个数。- 最终我们需要的结果是
dp[amount]。
② 确定递推公式
对于凑成金额 j,如果我们使用了硬币 coin,那么剩下的金额就是 j - coin。
此时凑成金额 j 的硬币数就是:dp[j - coin] + 1(加 1 是因为用了这一枚 coin)。
我们要找的是最少数,所以要在所有可能的硬币选项中取最小值:
注意:只有当
j - coin >= 0且dp[j - coin]是可达到的(不是初始的极大值)时,才进行转移。
③ dp 数组如何初始化
dp[0] = 0:凑足金额为 0 需要 0 枚硬币。- 非零下标初始化:因为我们要取的是最小值(
min),所以非零位置应初始化为一个极大值。- 在 Python 中可以用
float('inf')。 - 或者初始化为
amount + 1,因为即使全部用 1 元硬币,数量也不会超过amount。
- 在 Python 中可以用
④ 确定遍历顺序
由于硬币数量是无限的,这是一个完全背包问题。
-
外层循环:遍历金额
j从1到amount。 -
内层循环:遍历硬币列表
coins。(注:求最少硬币数时,先遍历硬币还是先遍历金额其实都可以,但通常先遍历金额更符合“从小问题推到大问题”的直觉。)
⑤ 举例推导 dp 数组
以 coins = [1, 2, 5], amount = 5 为例:
dp[0] = 0j = 1:dp[1] = dp[1-1] + 1 = 1j = 2:- 选 1:
dp[2-1] + 1 = 2 - 选 2:
dp[2-2] + 1 = 1 - 取最小:
dp[2] = 1
- 选 1:
j = 3:- 选 1:
dp[3-1] + 1 = 1+1 = 2 - 选 2:
dp[3-2] + 1 = 1+1 = 2 - 取最小:
dp[3] = 2
- 选 1:
- … 依此类推直到
j = 5,最后dp[5] = 1(直接用一枚 5 元)。
1 | class Solution: |
139. 单词拆分

假设输入为 s = "leetcode", wordDict = ["leet", "code"]。
① 确定 dp 数组(dp table)以及下标的含义
dp[i]:字符串 的前 个字符(即子串s[0:i])是否可以被拆分为字典中的单词。dp[i]为布尔类型(True或False)。- 最终目标:计算出
dp[len(s)]。
② 确定递推公式
如果 dp[j] 是 True(代表前 个字符已经可以拆分成功),那么我们只需要判断剩下的子串 s[j:i] 是否在字典中。
如果 s[j:i] 也在字典中,那么 dp[i] 就可以被确认为 True。
递推公式:
dp[i] = True(如果存在一个 ,使得dp[j] == True且s[j:i]在wordDict中)
③ dp 数组如何初始化
dp[0] = True:空字符串默认可以拆分(这是递推的基石,如果没有它,后面所有的dp[i]都会永远是False)。- 其他下标初始化为
False。
④ 确定遍历顺序
由于 dp[i] 依赖于它之前的状态 dp[j],所以必须从前往后遍历。
- 外层循环:遍历子串的结束位置 (从 1 到
len(s))。 - 内层循环:遍历分割点 (从 0 到 )。
⑤ 举例推导 dp 数组
以 s = "leetcode", wordDict = ["leet", "code"] 为例:
-
dp[0] = True -
到 :找不到任何 满足条件。(
"l","le","lee"都不在字典里) -
:当 时,
dp[0]是True且s[0:4] ("leet")在字典中,所以dp[4] = True。 -
到 :继续向后尝试。
-
:当 时,
dp[4]是True且s[4:8] ("code")在字典中,所以dp[8] = True。最终返回
True。
1 | class Solution: |
300. 最长递增子序列

假设输入数组为 nums = [10, 9, 2, 5, 3, 7, 101, 18]。
① 确定 dp 数组(dp table)以及下标的含义
dp[i]:以nums[i]结尾的最长严格递增子序列的长度。- 注意:一定要包含
nums[i]这个元素,这样在后续状态转移时,我们才有明确的参照点。 - 最终结果:不是
dp[n-1],而是整个dp数组中的最大值max(dp)。
② 确定递推公式
对于 dp[i],我们需要检查在它之前的所有位置 j(即 ):
-
如果
nums[i] > nums[j]:说明nums[i]可以接在以nums[j]结尾的子序列后面,构成一个新的递增子序列。此时长度为dp[j] + 1。 -
我们需要在所有满足条件的
j中取最大值。 -
递推公式:
③ dp 数组如何初始化
- 每个元素自身都可以看作一个长度为 1 的递增子序列。
- 因此,
dp数组的所有位置都应初始化为 1。
④ 确定遍历顺序
dp[i]取决于它之前的状态dp[j],所以必须从前向后遍历。- 外层循环 :从 遍历到 。
- 内层循环 :从 遍历到 。
④ 举例推导 dp 数组
以 nums = [10, 9, 2, 5, 3] 为例:
-
i = 0:nums[0]=10,dp[0]=1 -
i = 1:nums[1]=9。,不更新。dp[1]=1 -
i = 2:nums[2]=2。,不更新。dp[2]=1 -
i = 3:nums[3]=5。- 时不满足。
- 时,,
dp[3] = max(1, dp[2] + 1) = 2。
-
i = 4:nums[4]=3。-
时,,
dp[4] = max(1, dp[2] + 1) = 2。最终
max(dp)为 2。
-
1 | class Solution: |
152. 乘积最大子数组

既然你强调要严格执行“动态规划五步曲”,那我们就把这套标准模版套用到 152. 乘积最大子数组 上。这道题是五步曲训练的绝佳素材,因为它涉及到了状态的“双生”关系。
152. 乘积最大子数组 —— 动态规划五步曲
假设输入数组为 nums = [2, 3, -2, 4]。
① 确定 dp 数组(dp table)以及下标的含义
由于乘法中存在“负负得正”的情况,仅仅记录最大值是不够的,我们必须同时记录最小值(因为当前的最小值一旦乘以一个负数,可能瞬间变成最大值)。
dp_max[i]:表示以nums[i]结尾的连续子数组的最大乘积。dp_min[i]:表示以nums[i]结尾的连续子数组的最小乘积。- 注意:子数组必须连续且包含
nums[i]。
② 确定递推公式
对于第 个数 ,dp_max[i] 只能从以下三个值中产生:
- 不接前面的:直接就是 。
- 接前面的大值:(当 时有效)。
- 接前面的小值:(当 且之前有大负数时有效)。
因此递推公式为:
③ dp 数组如何初始化
- 第一个数无法向前结合,所以它既是最大也是最小:
- 其他位置可以根据这个起点开始推导。
④ 确定遍历顺序
- 因为 的值严谨依赖于 的值,所以必须从前往后遍历。
- 从 遍历到 。
⑤ 举例推导 dp 数组
以 nums = [2, 3, -2, 4] 为例,手动推导过程如下:
| i | nums[i] | dp_max[i] | dp_min[i] | 说明 |
|---|---|---|---|---|
| 0 | 2 | 2 | 2 | 初始化 |
| 1 | 3 | 延续增长 | ||
| 2 | -2 | 遭遇负数,极值反转 | ||
| 3 | 4 |
1 | class Solution: |
416. 分割等和子集

1 |
