0/1背包 滚动数组 深入理解
0/1背包🔺
问题
背包容量 = 4
| 物品 | 重量 | 价值 |
|---|---|---|
| 0 | 1 | 15 |
| 1 | 3 | 20 |
| 2 | 4 | 30 |
二维
-
下标含义:
dp[i][j] = 0...i之间的物品任意取,放到容量为j的背包中,能获得的最大价值。 -
递推公式:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]); -
初始化
for (int j = weight[0]; j <= bagweight; j++) { dp[0][j] = value[0]; } -
遍历顺序
-
打印、模拟
完整代码
public class BagProblem {
public static void main(String[] args) {
int[] weight = {1,3,4};
int[] value = {15,20,30};
int bagSize = 4;
testWeightBagProblem(weight,value,bagSize);
}
/**
* 动态规划获得结果
* @param weight 物品的重量
* @param value 物品的价值
* @param bagSize 背包的容量
*/
public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){
// 创建dp数组
int goods = weight.length; // 获取物品的数量
int[][] dp = new int[goods][bagSize + 1];
// 初始化dp数组
// 创建数组后,其中默认的值就是0
for (int j = weight[0]; j <= bagSize; j++) {
dp[0][j] = value[0];
}
// 填充dp数组
for (int i = 1; i < weight.length; i++) {
for (int j = 1; j <= bagSize; j++) {
if (j < weight[i]) {
/**
* j容量放不下物品i
*/
dp[i][j] = dp[i-1][j];
} else {
/**
* 当前背包的容量可以放下物品i
* 那么此时分两种情况:
* 1、不放物品i
* 2、放物品i(放的话要留出足够的空间
* 比较这两种情况下,哪种背包中物品的最大价值最大
*/
dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);
}
}
}
// 打印dp数组
for (int i = 0; i < goods; i++) {
for (int j = 0; j <= bagSize; j++) {
System.out.print(dp[i][j] + "\t");
}
System.out.println("\n");
}
}
}
二维dp中,i和j的内外层可以调换,即先遍历背包和先遍历物品是一样的,因为不会影响依赖关系。
一维(滚动数组)
由二维dp的递推公式dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);可以看出,递推过程是从上一层的数据传递到下一层的数据。所以我们可以只用一维数组dp[j]来记录dp[i][j]的状态,在更新的过程中不断用新的数据dp[j] (dp[i][j])覆盖掉旧的数据dp[j] (dp[i-1][j])。即递推公式从dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])变成了dp[i][j] = max(dp[i][j], dp[i][j-weight[i]] + value[i])。
但是可以发现,dp[i][j]取决于其上一层的正上方(dp[i-1][j])及左上方(dp[i-1][j-weight[i]])。如果按照正序遍历,会修改掉左上方的数据(也就意味着会重复取同一个元素)。因此,遍历顺序为i正序,j倒序。
完整代码
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagWight = 4;
testWeightBagProblem(weight, value, bagWight);
}
public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){
int wLen = weight.length;
//定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
int[] dp = new int[bagWeight + 1];
//遍历顺序:先遍历物品,再遍历背包容量
for (int i = 0; i < wLen; i++){
for (int j = bagWeight; j >= weight[i]; j--){
//当j < weight[i]时,dp[j]不变,因此省略
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
//打印dp数组
for (int j = 0; j <= bagWeight; j++){
System.out.print(dp[j] + " ");
}
}
一维dp中,i和j的内外层不能调换,必须先遍历物品嵌套遍历背包容量。因为一维dp时,j必须倒序遍历,这样会影响依赖关系。
因此,01背包一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历。
一维和二维还有一个代码的差别,二维的i从1开始,因为单独初始化了i=0时的数据,一维的i从0开始,因为在循环的过程中初始化了i=0时的数据(本来是0,max更新时更新成大的)。
0/1背包的应用
难点在于怎么想到题目需要用0/1背包来做
据说:「从序列中选择子序列使得和接近target」系列的题目,一般都是双向dfs或者01背包问题来完成。
1. 分割等和子集
分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
提示:
- 1 <= nums.length <= 200
- 1 <= nums[i] <= 100
代码及思路:
-
是否能分割成两个等和子集?-> 是否能找出一个和为
total/2的子集?(剩下的子集的和自然也为total/2) -
转化为背包问题
-
物品:数组元素,背包:元素的和,
value[i]和weight[i]都是nums[i] -
dp[i] = 容量为i的背包,能存放的最大价值
-
只有当
dp[total / 2] == total / 2时,才能返回true
class Solution {
//找出和为 总和/2 的子集
//法二:0/1背包
//1. dp[j] = 背包容量为j时,能存放的最大价值;只有当 dp[total/2] == total/2 时返回true
//2. dp[j] = max(dp[j], dp[j-weight[i]] + value[i]),即 dp[j] = max(dp[j], dp[j-nums[i]] + nums[i]);这里的weight和value都是nums
//3. 初始化:在循环过程中进行i=0时的初始化
//4. 遍历顺序:j必须倒序->必须i在外层j在内层
public boolean canPartition(int[] nums) {
int total = Arrays.stream(nums).sum(); //总和
if(total % 2 == 1) return false;
int target = total / 2;
int n = nums.length;
int[] dp = new int[target + 1];
for(int i=0; i<nums.length; i++) {
for(int j=target; j>=nums[i]; j--) {
dp[j] = Math.max(dp[j], dp[j-nums[i]] + nums[i]);
}
}
return dp[target] == target;
}
}
2. 最后一块石头的重量II
最后一块石头的重量II
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
- 如果 x == y,那么两块石头都会被完全粉碎;
- 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
示例 1:
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
示例 2:
输入:stones = [31,26,33,21,40]
输出:5
提示:
- 1 <= stones.length <= 30
- 1 <= stones[i] <= 100
代码及思路:
class Solution {
//必然只会剩下一颗石头,怎么让剩下的那块石头的重量最小?
//尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了
//也就是和416.分割等和子集很像
//1.dp[j] = 容量为j的背包,能装的最大价值
//2.dp[j] = max(dp[j], dp[j-stones[i]] + stones[i])
//3. 初始化:在循环过程中进行i=0时的初始化
//4. 遍历顺序:一维, j必须倒序->必须i在外层j在内层
//所求的为背包容量为 总和的一半 时, 能装的最大价值 与 剩下的价值 的 差值
public int lastStoneWeightII(int[] stones) {
int total = Arrays.stream(stones).sum();
int bagWeight = total / 2;
int[] dp = new int[bagWeight + 1];
for(int i=0; i<stones.length; i++) {
for(int j=bagWeight; j>=stones[i]; j--) {
dp[j] = Math.max(dp[j], dp[j-stones[i]] + stones[i]);
}
}
return Math.abs(total - 2 * dp[bagWeight]);
}
}
3. 目标和
给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
代码及思路:
- 求组合问题:
dp[j] += dp[j-nums[i]];
class Solution {
//将数组nums分为两个子集:+子集A -子集B
//sum(A) - sum(B) == target
//sum(A) + sum(B) == sum(nums)
//可以推出:sum(A) == [target + sum(nums)] / 2
//此时问题就转化为,装满容量为x的背包,有几种方法
//dp[j] = 使用下标为0...i的nums[i], 装满容量为j的包, 有dp[j]种方法
//dp[j] += dp[j-nums[i]], nums[i]:1->j (所有求组合类的问题,都是类似于这种)
//dp[0] = ? dp[0]是所有结果累加的源头,不能设为0
public int findTargetSumWays(int[] nums, int target) {
int sum = Arrays.stream(nums).sum();
if((target + sum) % 2 == 1) return 0; //子集的和不能为小数
if(Math.abs(target) > sum) return 0; //所有元素都是+/-也不能达到target
int bagSize = (target + sum) / 2;
int[] dp = new int[bagSize + 1];
dp[0] = 1;
for(int i=0; i<nums.length; i++) {
for(int j=bagSize; j>=nums[i]; j--) {
//装满j有几种方法 = 装满每一个比j小的容量有几种方法, 并且这个比j小的容量加上nums[i]就等于j了
dp[j] += dp[j-nums[i]];
}
}
return dp[bagSize];
}
}
关于如何判断背包类型

背包问题的基本条件:
- 背包
最大容量v
- 物品
价值w
体积v
每个物品的数量
根据每个物品的数量划分背包问题的类型。
举个例子
4. 一和零
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例 2:
输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。
提示:
1 <= strs.length <= 600
1 <= strs[i].length <= 100
strs[i] 仅由 '0' 和 '1' 组成
1 <= m, n <= 100
解题思路:
本题中strs 数组里的元素就是物品,每个物品都是一个!
而m 和 n相当于是一个背包,两个维度的背包。
具有不同数量的0和1的字符串就是不同大小的待装物品。
代码:
class Solution {
//符合0/1背包的条件
//背包容量:m个0,n个1
//物品:strs[i]
//value:1(元素个数)
//weight:0和1的个数
//dp[j] = 容量为j的背包能获得的最大价值 -> dp[i][j] = 容量为i个0、j个1的背包最多能包含的元素个数
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m+1][n+1];
for(String str : strs) { //遍历物品
int weight0 = 0, weight1 = 0; //获取当前物品的weight
for(int i=0; i<str.length(); i++) {
if(str.charAt(i) == '0') {
weight0++;
} else {
weight1++;
}
}
for(int i=m; i>=weight0; i--) { //倒序遍历背包容量
for(int j=n; j>=weight1; j--) {
dp[i][j] = Math.max(dp[i][j], dp[i-weight0][j-weight1] + 1);
}
}
}
return dp[m][n];
}
}
0/1背包总结 (代码随想录)

浙公网安备 33010602011771号