代码随想录算法训练营第42天 || 01背包问题 || 416. 分割等和子集

代码随想录算法训练营第42天 || 01背包问题 || 416. 分割等和子集

01背包问题

416.分割等和子集1

对于这么多背包问题,面试只需要掌握01背包完全背包基本足够,最多再了解一下多重背包即可。其他很多都是竞赛级别难度。

而这些背包问题中,01背包又是最基础且最重要的。

01背包

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品只能用一次**,求解将哪些物品装入背包里物品价值总和最大。

暴力解法

每个物品只有两种状态取或者不取,所以我们可以使用回溯法搜索所有的情况,时间复杂度为 O ( 2 n ) O(2^n) O(2n)

二维dp数组动规法

五部曲

  1. 确定dp数组下标及其含义

    dp[i][j]表示从下标0-i的物品中任意取,放进容量为j的背包中的最大价值

  2. 确定递推公式

    由dp数组的下标含义,我们可以想到两个方向来推dp[i][j]

    • 不放物品i:此时最大价值为:dp[i-1][j]
    • 放物品i:此时最大价值,我们可以拆分成两部分物品i的价值 value[i] 和除去物品i的价值 dp[i-1][j-weigh[i]](也就是容量减去i重量的最大价值)

    由此,我们不难得到递推公式:dp[i][j] = Integer.max(dp[i-1][j],dp[i-1][j-weigh[i]]);

  3. 初始化dp数组

    我们由递推公式可以发现,当前位置的值是有左上方或正上方推来的。所以我们一开始可以先初始化边缘位置的数值。其余位置不用管,因为我们只会比较左上方和正上方的结果,与自身无关。与之前写的一些动规和不断更新自身并比较自身的题目不同。

    • 对于j = 0这一列,不难得出dp[i][0] = 0
    • 对于i = 0这一行,只要容量足够放 i 物品 (j >= weigh[i]),dp[0][i] = value[i],否则赋值为0
  4. 遍历顺序

    背包问题的遍历顺序是非常讲究的,这里追究起来比推导公式还要复杂

    不过对于本题的01背包问题,其实先遍历物品还是背包都可以,因为递推公式用到的都是在左(正)上方,没涉及更复杂的情况。

  5. 举例推导dp数组

    真正写代码的时候,建议先手动模拟一下,再写代码!

public class test {
    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) {
        int m = weight.length;
        int[][] dp = new int[m][bagSize + 1];
        //初始化操作
        for (int i = 1; i <= m; i++) {
            dp[0][i] = i >= weight[0] ? value[0] : 0;
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j <= bagSize; j++) {
                dp[i][j] = Integer.max(dp[i - 1][j], j - weight[i] > 0 ? dp[i - 1][j - weight[i]] + value[i] : 0);
                System.out.println(i + " " + j + " " + dp[i][j]);
            }
        }
    }
}
一维滚动数组

压缩二维dp数组到一维,我们可以通过覆盖的方式,将dp[i]层覆盖到dp[i-1]上,此时得到的dp[j],其中j表示容量,dp[j]则表示从下标0-i(循环递增i)物品中任意取,放进容量j的背包的最大价值。

动规五部曲

  1. 确定dp数组及其下标含义

    dp[j] 表示:容量为j的背包中,所背的物品价值最大可以为dp[j]

  2. 确定递推公式

    与二维类似,放与不放的比较谈论

    • 不放第i个物品:dp[j](上一状态)
    • 放第i个物品:dp[i-weight[i]]+value[i]

    所以,可得递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

  3. 初始化dp数组

    dp[0] = 0无异议,但其余部分呢?

    观察递推公式,我们发现比较的是上一状态的dp[j] 和 放第 i 个物品的情况

    所以我们可以初始化<=0的数均可,这样就不会影响下一阶段的比较

    因为数组默认初始化为0,所以这一步初始化我们可以不体现。

  4. 一维dp数组遍历顺序

    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]);
        }
    }
    
    • 先遍历物品,再遍历背包
      • 因为这样才可以一层层替换,如果反过来其实整个递推公式都没有意义了,上一状态无法使用
    • 倒叙遍历背包容量
      • 这里有别于此前二维遍历,二维遍历正序倒叙均可,因为它只与上一层有关,本层怎么遍历都不影响
      • 如果正序遍历,那么递推公式中的加入i物品的dp[j - weight[i]]就是已经加过i物品的状态,因为正序遍历这个下标肯定在当前下标的左边,然后我们又+value[i],显然重复加入了。
  5. 举例推导dp数组

一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下:

动态规划-背包问题9

代码:

public class test {
    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[] dp = new int[bagWeight + 1];
        //初始化为0,没体现
        for (int i = 0; i < value.length; i++) {
            for (int j = bagWeight; j > 0; j--) {
                if (j - weight[i] >= 0)
                    dp[j] = Integer.max(dp[j], dp[j - weight[i]] + value[i]);
                System.out.println(i + " " + j + " " + dp[j]);
            }
        }
    }
}

面试问题

  1. 二维实现—>for循环可不可以颠倒
  2. 一维实现—>for循环可不可以颠倒顺序—>为什么要逆序遍历?能不能正序?—>为什么一维正逆序均可?

416. 分割等和子集

题目介绍:

给你一个 只包含正整数非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

个人思路

在背包容量、物品价值、物品重量的对应关系上没能处理好,不能得到一个合适的01背包模型。

题解解析

本题是01背包问题,因为每个数字只能用一次

要确定的四点:

  • 背包的体积为sum/2
  • 背包要放的元素 重量为 元素的数值,价值也为元素的数值(很关键)
  • 背包正好装满,说明找到了总和为sum/2的子集
  • 背包的每一个元素都是不可重复放入的

动规五部曲

  1. 确定dp数组及其下标含义

    dp[j]表示容量为j的背包所能装的最大价值,for循环物品i表示放不放物品i

  2. 确定递推公式

    dp[j] = Integer(dp[j],dp[j-nums[i]+nums[i]);

    物品的重量和价值都是nums[i]

  3. 初始化dp数组

    默认初始化为0 即可,隐式初始化

  4. 遍历顺序

    一维滚动数组:

    • 先遍历物品,再遍历背包
    • 背包容量逆序遍历(避免重复加入)
  5. 打印dp数组检验

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++)
            sum += nums[i];
        if (sum % 2 == 1)
            return false;
        sum /= 2;
        int[] dp = new int[sum + 1];
        //背包容量 == 总和一半
        for (int i = 0; i < nums.length; i++) {
            for (int j = sum; j > 0; j--) {
                if (j - nums[i] >= 0)
                    dp[j] = Integer.max(dp[j], dp[j - nums[i]] + nums[i]);
                if (dp[j] == sum)//过程中,背包能装满/价值满足就返回结果
                    return true;
            }
        }
        //遍历完所有情况都不能装满背包/价值满足
        return false;
    }
}

回顾本题,关键点在于如何确定物品的价值和重量及背包的容量,从而得到一个合适的01背包模型。

  • 物品重量和物品数值相等、背包容量与目标数值相等?
    • 为了能正确放入物品,避免放入过大的物品,导致后序的背包遍历都无意义
  • 物品价值 == 数值?
    • 为了检验满足背包放满的条件,用价值"放满"来等价于背包放满
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值