代码随想录算法训练营第四十一天|01背包理论基础 416. 分割等和子集

01背包理论基础

  • 01背包问题:

    • 有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大
  • 01背包问题的解法:
    • 暴力解法:回溯算法
      • 有n个物品,其中每件物品的状态只有取和不取,使用回溯法搜索出所有的情况,所以时间复杂度为O(2^n)
      • 使用回溯算法的时间复杂度是指数级别,所以需要使用动态规划的解法来进行优化
    • 动态规划:
      • 动态规划的解法有两种:一种是使用二维dp数组,另一种是使用一维dp数组
      • 二维dp数组:dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少
      • 一维dp数组:dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]
  • 01背包问题的重要性:
    • 背包问题的理论基础重中之重是01背包,一定要理解透
    • 01背包和完全背包的区别是:01背包的每个物品数量只有一个,而完全背包每个物品的数量有无数个
  • 还有其他的背包问题:
    • 01背包:有n种物品,每种物品只有一个
    • 完全背包:有n种物品,每种物品有无限个
    • 多重背包:有n种物品,没有物品的个数各不相同

二维dp数组

对于动态规划的思路:一定要时刻记着5个步骤:1、确定dp数组以及下标的含义;2、确定递推公式;3、dp数组如何初始化;4、确定遍历顺序;5、打印dp数组

1、确定dp数组以及下标的含义
        二维dp数组是基础,在此基础上可以优化为一维dp数组,所以要先弄清楚二维dp数组

        dp[i][j]:  背包中物品的总价值
        i 代表:物品遍历到物品 i
        j 代表:目前背包的容量

2、确定递推公式
        要求递推公式,就是要求,当前背包中的最大值该怎么获得
        有两个方向:

  • 由dp[i - 1][j]推出:这种情况代表,当前物品 i 放不到背包中,只能获取前 i-1 个物品放入后,所能得到的最大价值
  • 由dp[i - 1][j - wight[i]]推出:这种情况代表:当前的物品 i 可以放到背包中,物品 i 的价值为 value[i] , 此时还需要知道 放了物品 i 之后,所剩容量( j - wight[i] ) 还能放的最大价值为 ( dp[i - 1][j - wight[i]] ) ,两者之和就是遍历到当前物品,容量为j的背包中可以存放的最大价值
  • 当前背包的最大容量就这两种情况,所以在这两种情况中选取最大值就可以
  • 最终的递推公式为:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);                    

3、dp数组如何初始化
        初始化dp数组的时候,一定要和dp数组的定义吻合

        首先,当背包容量为0时,不论遍历到哪个物品,背包中的最大价值都是0,即dp[i][0] 全为0 

        其次,从递推公式中可以知道,dp[i][j] 的两种状态都是由 dp[i - 1][j] 来获取的,所以 dp[0][j]的状态一定要进行初始化,因为0-1为 -1,这种情况出界了

         最后,是除左边界和右边界的所有情况的初始化,这些位置其实赋值任何都可以,因为在后面填充数组的过程中,其中的值还是会被 递推公式中的值所覆盖,跟本身初始化是什么值无关

4、确定遍历顺序
这里遍历右两种情况:

  • 先遍历物品,再遍历背包重量
  • 先遍历背包重量,再遍历物品

这两种情况都是可以的,但是先遍历物品更好理解

因为dp[i][j] 每次都是获取正上方或者左上角的数据,不会被右方的数据影响

5、打印dp数组
表格中的最后一个元素就是需要求的结果

 最好根据递推公式手动推导一下这个表格就很清楚了

最终代码

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]) {
                    /**
                     * 当前背包的容量都没有当前物品i大的时候,是不放物品i的
                     * 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
                     */
                    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数组
         一维dp数组是对二维dp数组情况的优化,因为dp[i][j] 是在dp[i - 1][j] 的基础上得到的,所以完全可以只维护一维dp数组,可以理解成是一个滚动数组,每遍历一个物品,一维dp数组全部更新一次

1、确定dp数组的定义
        一维数组:dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]

2、确定递推公式
        根据前面二维dp数组的递推公式来推导一下,一维dp数组的递推公式,还是两种情况:

  • 由dp[j]推出,这种情况代表没有放物品 i ,所以还是原来的值,代表放入容量为 j 的前 i-1 个物品的最大价值
  • 由dp[j - weight[i]],这种情况代表放了物品 i ,物品i 的价值是 value[i] , 剩余的容量(j - weigth[i])能放的 i - 1 个物品的价值为 dp[j - weight[i]],所以放物品 i 总的价值为 dp[j - weight[i]] + value[i]
  • 所以 dp[j] = max(dp[j] ,  dp[j - weight[i]] + value[i]);

3、初始化dp数组
        对于dp[0] ,也就是 背包容量为 0 的时候,肯定应该赋值为 0 ,即 dp[0] = 0

        但是对于非0下标应该怎么赋值呢,其实dp数组在推导的过程中一定是取最大数的,如果题目给的价值都是正整数,那么非0下标都初始化0就可以了

        这样才能让dp数组在递推公式的过程中取的最大价值,而不是被覆盖(比如初始化100 就会被覆盖)

        所以假设物品的价值都是大于0的,所以dp数组初始化的时候,都初始化成0 就可以了

4、遍历顺序
        这里的遍历顺序是从后向前进行遍历,因为dp[j - weight[i]]这种情况要用到上一个物品对应的数组前面的值

        这种倒序遍历保证了物品 i 只被放入一次,如果从前向后遍历会造成物品被多次加入背包的情况

        要清楚的是:
        二维dp数组的时候是有两种写法的,一种是先遍历物品再遍历背包;另一种是先遍历背包再遍历物品,而且从前向后遍历,或者从后向前遍历都是可以的
        一维dp数组是根据二维dp数组中的先遍历物品再遍历背包推导来的,而且每次获取下一个物品对应的dp[i][j]时会根据前面的dp[i-1][j-weight[i]]获得,这个数组是在当前层的左上方,所以当是一维数组的时候,移动要从后向前进行遍历,前面的数在这一层更新中还要继续使用呢

        并且如果在一维数组中采用从前向后的遍历时,会导致物品被多次添加进背包,因为在确定下标大的元素的时候会用到下标小的元素,所以下标小的元素要最后才更新,所以要从后向前遍历

        二维dp数组中,当前层是根据上一层来推导出来的,当前层的每一个值不受上一层的值得影响,两层得数据是完全隔离开来的,所以正序遍历和倒序遍历都可以
        一维dp数组中,数组的内存空间是重复利用的,如果正序遍历,当前计算出来的值就会和旧值产生冲突

5、打印dp数组

public class BagProblem2 {
    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[] dp = new int[bagSize + 1];
 
        // 初始化dp数组
        // 将dp[]数组初始化为0,默认就是,不用操作
 
        // 遍历填充dp数组
        // 外层循环代表几个物品,循环几次
        // 内层循环更换最大值
        for (int i = 0; i < weight.length; i++) {
            for (int j = bagSize; j >= weight[i]; j-- ) {
                dp[j] = Math.max(dp[j],dp[j-weight[i]]+value[i]);
            }
 
            // 打印dp数组
            System.out.print("物品" + i + "\t");
            for (int j = 0; j <= bagSize; j++) {
 
                System.out.print(dp[j] + "\t");
            }
            System.out.println("\n");
        }
 
    }
}

416. 分割等和子集 

链接:. - 力扣(LeetCode)

细节:

class Solution {
    public boolean canPartition(int[] nums) {
 
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
 
        if ( sum % 2 != 0) {
            return false;
        }
 
        // 目标和为
        int target = sum / 2;
 
        // 创建dp[j]数组
        int[] dp = new int[target + 1];
 
        // 填充dp数组
        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 target == dp[target];
 
    }
}

  • 22
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值