算法训练day41-动态规划-背包问题基础

01背包

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

解题思路

如果用暴力解法,那么对于没个物品有两种情况,取或者不取,最终的时间复杂度就是2^n,这已经是指数级别的时间复杂度了,所以不能采用。

为了方便理解,这里假设 n =3 ,w=4

采用动态规划的五步分析法:

  1. 确定dp[i][j]数组的含义

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

  1. 确定递推公式

不放物品i时:背包容量为j,放入物品i会导致背包超重,此时dp[i][j] = dp[i-1][j]

放物品i时: 此时背包最大价值应该是不放物品i时的最大价值加上物品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. dp数组初始化

从递推公式含义出发,初始化如下:

当j=0时,背包容量为0,此时总价值dp[i][0]一定为0;

当i=0时,j<weight[i] ,那么dp[0][j] = 0;

当i=0时,j>=weight[i],那么dp[0][j] = value[i];

其余默认初始化0

  1. 确定遍历顺序

因为递推公式是由dp[i-1][j]和dp[i - 1][j - weight[i]]推导出来的。所以遍历顺序并不影响结果。既可以先遍历物品,也可以先遍历背包重量。

先遍历物品代码:

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. 完整测试代码示例-java

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");
        }
    }
}
  1. 一维dp数组

上面二维数组的递推公式为 dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])。

我们发现可以将dp[i-1]的那一行copy到dp[i]上,此时公式变为dp[i][j] = max(dp[1][j],dp[i][j-weight[i]]+value[i])。这个时候其实可以只用一个一维数组dp[j]来代替dp[i][j].

对于dp[j]有以下分析:

1)确定dp[j]含义

dp[j] 表示背包最大重量为j时,所能装的最大价值

2)dp[j] 的递推公式

dp[j] 可以由两种情况推导出来,第一种就是dp[j],不放物品i;另一种就是加上物品i的价值

dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

3)初始化

结合定义知道,dp[0] = 0;其他也都初始化为0.

4)遍历顺序

与二维数组不同的是,当遍历背包容量时,必须从大到小遍历,否则会导致物品i重复放入。

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

    }
}

5)完整测试代码-java

    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--){
                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] + " ");
        }
    }

416. 分割等和子集

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

注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200

示例 1:

  • 输入: [1, 5, 11, 5]

  • 输出: true

  • 解释: 数组可以分割成 [1, 5, 5] 和 [11].

解题思路

这道题可以抽象为经典的01背包问题。

首先数组nums 的元素总和如果可以平分为2,说明该总和sum是偶数。其次分成的两个子集和相等说明每个子集和target= sum/2;

于是我们可以将target看做背包最大容量,数组nums中的每个元素看做物品价值和重量。再按照求解01背包问题的方法就能加以解决了。

代码示例-java

class Solution {
    public boolean canPartition(int[] nums) {
        // 判空
        if(nums == null ||nums.length==0){
            return false;
        }
        
        int n = nums.length;
        int sum = 0;
        for(int i=0;i<n;i++){
           sum += nums[i];
        }
        // 总和为奇数,不能平分
        if(sum % 2!=0){
            return false;
        }
        // 取得集合总和的一半,看做为背包总容量
        int target  = sum/2;
        // 定义dp数组
        int[] dp = new int[target+1];
        // 初始化
        dp[0] = 0;
        // 遍历
        for(int i=0;i<n;i++){
            // 背包容量必须从大到小遍历
            for(int j=target;j>=nums[i];j--){
                // 物品i的价值和重量都是nums[i]
                dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
            }
        }

       return dp[target] == target;

    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值