dp背包总结
动态规划背包问题详解:0-1背包与完全背包
引言
背包问题是动态规划中的经典问题,也是面试中高频考察的算法模型。根据物品是否可以被重复选取,背包问题主要分为0-1背包和完全背包。理解这两种背包问题的原理、递推公式、初始化以及遍历顺序,是解决一系列相关题目(如分割等和子集、最后一块石头的重量、目标和、零钱兑换等)的基础。
本文将从理论基础出发,详细讲解0-1背包和完全背包的解法,并对比两者的核心区别。
一、0-1背包问题
1.1 问题描述
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],价值是value[i]。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
例如:
- 背包最大重量:4
- 物品:
- 物品0:重量1,价值15
- 物品1:重量3,价值20
- 物品2:重量4,价值30
问背包能背的最大价值是多少?
1.2 二维dp数组解法
1.2.1 确定dp数组含义
dp[i][j] 表示从下标为[0..i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
1.2.2 递推公式
对于物品i,有两种情况:
- 不放物品i:
dp[i][j] = dp[i-1][j] - 放物品i:
dp[i][j] = dp[i-1][j - weight[i]] + value[i]
取两者最大值:
dp[i][j] = max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i])
1.2.3 初始化
dp[i][0] = 0(背包容量为0,价值为0)- 对于第一行
i=0:- 当
j < weight[0]时,dp[0][j] = 0 - 当
j >= weight[0]时,dp[0][j] = value[0]
- 当
其他位置可以初始化为0(因为后续会被覆盖)。
1.2.4 遍历顺序
外层循环遍历物品,内层循环遍历背包容量。先遍历物品还是先遍历背包都可以,但通常先遍历物品更好理解。注意内层循环从小到大遍历。
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i-1][j];
else dp[i][j] = max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i]);
}
}
1.2.5 举例推导
以上述例子,最终dp[2][4] = 35。
1.3 一维dp数组(滚动数组)解法
为了优化空间,可以将二维数组压缩为一维数组。因为dp[i][j]只依赖于上一行的数据,我们可以只用一个一维数组,在遍历时从后向前更新,确保每个物品只被放入一次。
1.3.1 确定dp数组含义
dp[j] 表示容量为j的背包,所背物品的最大价值。
1.3.2 递推公式
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
1.3.3 初始化
dp[0] = 0,其他非0下标也初始化为0(因为价值都是正数)。
1.3.4 遍历顺序
外层遍历物品,内层遍历背包容量且必须倒序。倒序是为了保证每个物品只被放入一次(如果正序,同一物品可能被多次放入)。
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量,倒序
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
1.3.5 举例
用物品0、1、2依次更新,最终dp[4] = 35。
二、完全背包问题
2.1 问题描述
与0-1背包唯一不同:每件物品都有无限个(可重复放入)。背包容量为W,物品重量和价值同上,求最大总价值。
2.2 一维dp数组解法
完全背包也可以使用一维dp数组,但遍历顺序需要改变。
2.2.1 递推公式
与0-1背包一样:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
2.2.2 初始化
同样,dp[0] = 0,其他为0。
2.2.3 遍历顺序
外层遍历物品,内层遍历背包容量且必须正序。因为物品可以重复添加,所以需要从小到大的顺序,使得同一物品可以多次累加。
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量,正序
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
2.3 关于两层for循环的先后顺序
对于纯完全背包问题(即求最大价值),先遍历物品再遍历背包与先遍历背包再遍历物品都是可以的。因为无论哪种顺序,dp[j]的计算都依赖于之前的状态,且最终结果相同。
例如先遍历背包再遍历物品:
for(int j = 0; j <= bagWeight; j++) { // 遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
if (j - weight[i] >= 0)
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
但需要注意:当题目要求求组合数或排列数时,遍历顺序就至关重要。
- 求组合数(不讲究顺序):外层遍历物品,内层遍历背包(如零钱兑换II)
- 求排列数(讲究顺序):外层遍历背包,内层遍历物品(如组合总和IV)
这一点在解决具体题目时需要仔细区分。
三、0-1背包与完全背包的对比
| 特性 | 0-1背包 | 完全背包 |
|---|---|---|
| 物品数量 | 每个物品只能用一次 | 每个物品可以用无限次 |
| 一维dp遍历顺序 | 内层循环倒序 | 内层循环正序 |
| 两层for循环顺序 | 只能先遍历物品,再遍历背包(一维) | 先物品后背包或先背包后物品均可(纯最大价值) |
| 典型应用 | 分割等和子集、最后一块石头的重量II | 零钱兑换、完全平方数、单词拆分 |
四、总结
背包问题的核心在于递推公式和遍历顺序的理解。对于0-1背包,一维数组必须倒序遍历以保证每个物品只取一次;对于完全背包,正序遍历允许重复选取。此外,当问题涉及“装满背包有多少种方式”时,还需要根据组合或排列要求调整两层循环的先后顺序。
掌握了这两种基础背包模型,就能解决LeetCode上大量的动态规划题目。建议读者在学习过程中,多动手推导dp数组,理解每一步的状态转移,这样才能真正融会贯通。
希望这篇博客能帮助你彻底理清0-1背包和完全背包的精髓!如有疑问,欢迎留言讨论。

浙公网安备 33010602011771号