1. 动态规划专题精讲:三题联解
今天咱们来啃动态规划这块硬骨头,通过LeetCode 1049、494、474三道经典题目,带大家掌握背包问题的核心套路。这三题看似不同,实则都暗藏01背包的玄机,我会用同一套思维框架拆解,让你真正理解如何把实际问题转化为背包模型。
实战心得:动态规划题目往往有多个变种,建议按专题集中突破。我当初连续刷了20多道背包问题后,突然有种"开窍"的感觉,现在看到类似题目都能快速识别出背包模型的影子。
1.1 题目本质剖析
先看1049题"最后一块石头的重量II",题目要求将石头分成两堆,使两堆重量差最小。这实际上就是寻找最接近sum/2的子集和——标准的01背包问题。494题"目标和"需要给数字添加正负号达到目标值,可以转化为"找出和为特定值的子集个数"。而474题"一和零"则是二维费用的背包问题,需要同时考虑0和1的容量限制。
关键转换技巧:
- 1049:sum/2作为背包容量,stones作为物品重量
- 494:(sum + target)/2作为背包容量,nums作为物品
- 474:m和n作为二维容量,字符串作为物品
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 1049题深度解析
2.1 问题重述与建模
有若干石头,每个石头重量为stones[i],需要将这些石头分成两堆,使两堆重量差最小。最终返回这个最小差值。
例如stones = [2,7,4,1,8,1]:
- 最优分法是(2,7,1,1)和(4,8),差值为1
- 转化为背包问题:总重量sum=23,背包容量为11,求能装的最大重量
2.2 动态规划实现
python复制def lastStoneWeightII(stones):
total = sum(stones)
target = total // 2
dp = [0] * (target + 1)
for stone in stones:
for j in range(target, stone - 1, -1):
dp[j] = max(dp[j], dp[j - stone] + stone)
return total - 2 * dp[target]
关键点说明:
- dp数组含义:dp[j]表示容量为j的背包能装的最大重量
- 遍历顺序:物品正序,容量逆序(防止重复计算)
- 初始化:dp[0]=0,其他初始为0
- 状态转移:max(不选当前石头,选当前石头)
避坑指南:这里最容易出错的是遍历顺序。必须是外层遍历物品,内层逆序遍历容量。如果都正序遍历,就变成完全背包问题了。
2.3 复杂度分析
时间复杂度:O(n×target),其中n是石头数量
空间复杂度:O(target)
3. 494题解题思路
3.1 问题转化技巧
给定非负整数数组nums和目标整数target,向每个nums[i]添加+或-,使表达式结果为target。求共有多少种方法。
例如nums=[1,1,1,1,1], target=3:
- 有5种方法可以使表达式结果为3
关键转化:
设正数和为x,负数和绝对值为y,则有:
x - y = target
x + y = sum
=> x = (sum + target)/2
问题转化为:在nums中找出和为x的子集个数
3.2 动态规划解法
python复制def findTargetSumWays(nums, target):
total = sum(nums)
if (total + target) % 2 != 0 or abs(target) > total:
return 0
x = (total + target) // 2
dp = [0] * (x + 1)
dp[0] = 1
for num in nums:
for j in range(x, num - 1, -1):
dp[j] += dp[j - num]
return dp[x]
特殊处理:
- 检查(sum + target)是否为偶数
- 检查target绝对值是否超过sum
- dp[0]=1表示和为0有1种方法(什么都不选)
3.3 与1049的异同
相同点:
- 都是01背包问题
- 都需要计算sum/2或类似值作为target
不同点:
- 1049求最大价值,494求方案数
- 494需要特殊边界条件检查
- 494的dp数组初始化为[1]+[0]*x
4. 474题多维背包解法
4.1 问题描述
给定字符串数组strs和两个整数m和n,找出并返回strs的最大子集大小,该子集中最多有m个0和n个1。
例如:
输入:strs = ["10","0001","111001","1","0"], m = 5, n = 3
输出:4
解释:最多有5个0和3个1的子集是
4.2 二维费用背包实现
python复制def findMaxForm(strs, m, n):
dp = [[0] * (n + 1) for _ in range(m + 1)]
for s in strs:
zeros = s.count('0')
ones = len(s) - zeros
for i in range(m, zeros - 1, -1):
for j in range(n, ones - 1, -1):
dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1)
return dp[m][n]
关键点:
- dp[i][j]表示使用i个0和j个1能组成的最大子集大小
- 需要先统计每个字符串的0和1数量
- 双重逆序遍历保证每个字符串只使用一次
4.3 三维到二维的优化
原始思路可能是三维dp[i][j][k],但通过逆序遍历可以优化为二维。这是背包问题的常见空间优化技巧。
5. 通用解题框架
5.1 背包问题识别特征
当题目出现以下特征时,考虑01背包解法:
- 有限物品中选择部分
- 有某种容量限制
- 求最大价值或方案数
- 每个物品只能选或不选
5.2 标准解题步骤
- 确定背包容量和物品重量/价值
- 初始化dp数组(一维或二维)
- 遍历物品
- 逆序遍历容量(防止重复计算)
- 根据问题类型选择状态转移方程:
- 最大价值:dp[j] = max(dp[j], dp[j-w]+v)
- 方案数:dp[j] += dp[j-w]
- 返回最终结果
5.3 常见变种处理
- 完全背包:正序遍历容量
- 多维费用:增加dp维度
- 排列问题:调整遍历顺序
- 分组背包:增加物品组别维度
6. 调试与优化技巧
6.1 打印DP表
遇到问题时,打印dp数组查看中间状态:
python复制def print_dp(dp):
for row in dp:
print(row)
print()
6.2 边界条件检查
特别注意:
- sum的奇偶性
- target是否超出范围
- 空集的特殊情况
6.3 空间优化策略
从三维到二维的优化示例:
python复制# 优化前
dp = [[[0]*(n+1) for _ in range(m+1)] for __ in range(len(strs)+1)]
# 优化后
dp = [[0]*(n+1) for _ in range(m+1)]
7. 扩展练习建议
为了巩固背包问题,建议按顺序刷以下题目:
-
- 分割等和子集(基础01背包)
-
- 零钱兑换(完全背包)
-
- 零钱兑换II(方案数)
-
- 盈利计划(多维费用)
-
- 数位成本和为目标值的最大数字(特殊输出)
每道题建议先自己思考如何转化为背包问题,再对比题解。记录下自己的思考过程和易错点,这样的刷题效果最好。
