0/1背包 滚动数组 深入理解

0/1背包🔺

问题

背包容量 = 4

物品 重量 价值
0 1 15
1 3 20
2 4 30

二维

  1. 下标含义:
    dp[i][j] = 0...i之间的物品任意取,放到容量为j的背包中,能获得的最大价值。

  2. 递推公式:
    dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);

  3. 初始化
    for (int j = weight[0]; j <= bagweight; j++) { dp[0][j] = value[0]; }

  4. 遍历顺序

  5. 打印、模拟

完整代码

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];
    }
}

关于如何判断背包类型

背包问题类型

背包问题的基本条件:

  1. 背包

最大容量v

  1. 物品

价值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背包总结 (代码随想录)

0/1背包总结 代码随想录

posted @ 2023-09-05 15:34  shimmer_ghq  阅读(60)  评论(0)    收藏  举报