代码随想录算法训练营第四十二天|01背包问题,你该了解这些!01背包问题,你该了解这些!滚动数组、416. 分割等和子集

代码随想录 (programmercarl.com)

01背包问题,你该了解这些! 

01背包:每个物品只能够使用一次

代码随想录算法训练营第四十二天|01背包问题,你该了解这些!01背包问题,你该了解这些!滚动数组、416. 分割等和子集_第1张图片

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

dp[i][j]: 下标为[0, i]之间的物品,任取放进容量为j的背包里的价值大小

2.确定递推公式

(1)如果背包重量 < 物品重量

无法放物品i,价值为dp[i][j] = dp[i - 1][j];

(2)其他情况:

①不放物品i,价值为dp[i][j] = dp[i - 1][j];(跳过了这个物品,所以剩下的物品是在[0, i - 1]之间了

②放物品i, 价值为dp[i - 1][j - weight[i]] + value[i]

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

3.初始化

代码随想录算法训练营第四十二天|01背包问题,你该了解这些!01背包问题,你该了解这些!滚动数组、416. 分割等和子集_第2张图片

根据①可知,要想获得该位置的价值,要用到上方的值;

根据②可知,要想获得该位置的价值,要用到左上方的值(因为i - 1在上方,j - weight[i] + value[i]在左边,背包重量减轻了)。

所以,初始化最左边一列和最上边一行即可。

其中,最左边一列初始化为0,最上边一行初始化为物品0的价值(注意重量是否超过背包重量)。

4.遍历顺序

从前往后遍历,两个for循环可以调换顺序。

先遍历物品再遍历背包这个顺序更好理解

import java.util.Arrays;

/**
 * ClassName: Test
 * Package: PACKAGE_NAME
 */
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[][] dp = new int[weight.length][bagSize + 1];
        //初始化最上面一行
        for (int j = weight[0]; j <= bagSize; j++) {
            dp[0][j] = value[0];
        }
        //本来需要将最左边一列初始化为0,但其实整个二维数组在创建时就会初始化为0,所以可以省略
        //遍历物品和背包
        for (int i = 1; i < weight.length; i++) {
            for (int j = 1; j <= bagSize; j++) {
                if (j < weight[i]) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
                }
            }
        }
        // 打印dp数组
        for(int[] arr : dp){
            System.out.println(Arrays.toString(arr));
        }
/*        // 打印dp数组
        for (int i = 0; i < weight.length; i++) {
            for (int j = 0; j <= bagSize; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println("\n");
        }*/
    }
}

01背包问题,你该了解这些! 滚动数组  

当前层是通过上一层进行计算的,那我们就可以把上一层拷贝到当前层直接进行计算。

就把一个矩阵压缩成一行来计算,将二维降低到一维。

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

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

2.确定递推公式

(1)如果背包重量 < 物品重量

无法放物品i,价值为dp[j] = dp[j];

(2)其他情况:

①不放物品i,价值为dp[j] = dp[j];(跳过了这个物品,所以剩下的物品是在[0, i - 1]之间了)

②放物品i, 价值为dp[j - weight[i]] + value[i]

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

3.初始化

dp[0] = 0;        非零下标全部初始化为0

4.遍历顺序

dp[1] = dp[1 - 1] + 15 = 15;

dp[2] = dp[2 - 1] + 15 = 30;

说明dp[2]的时候把物品0放进背包里2次,所以不能从前往后遍历,需要从后往前遍历背包。

先遍历物品,再遍历背包。

从后往前遍历,两个for循环不可以调换顺序,只能先遍历物品再遍历背包。

数据循环滚动运用,可以尝试自己修改遍历顺序,打印dp数组尝试

【面试问题】

为什么二维背包的遍历顺序可以是先背包再物品,或者先物品再背包?

虽然两个for循环遍历的次序不同,但是dp[i][j]所需要的数据就是左上角,根本不影响dp[i][j]公式的推导。

为什么二维dp数组遍历的时候不用倒序呢?

因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖。

为什么一维背包的两个for循环不能调换顺序?为什么需要从后往前遍历?

右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。(二维的时候当前位置的值是由上面和左上角的值决定的,现在压缩为一维,覆盖了上面的值,就只由左边的值决定了)

倒序遍历是为了保证物品i只被放入一次。

public class Test {
    public static void main(String[] args) {
        int[] weight = {1, 3, 4};
        int[] value = {15, 20, 30};
        int bagWeight = 4;
        testWeightBagProblem(weight, value, bagWeight);
    }

    public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight) {
        //创建dp数组并进行初始化
        int[] dp = new int[bagWeight + 1];
        //递推公式
        for (int i = 0; i < weight.length; i++) {
            for (int j = bagWeight; j >= weight[i] ; j--) {//当j < weight[i]的时候,直接跳过,背包放不下了
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
            }
        }
        //遍历输出
        for (int j = 0; j <= bagWeight; j++) {
            System.out.print(dp[j] + " ");
        }
    }
}

416. 分割等和子集 

将该问题抽象为一个0-1背包问题,给定的非空数组数值nums[i]同时当作物品的weight和value。

其中背包的容量为j,是将数组数值相加求和 / 2得到的。

也就是说,这个分割等和子集问题变为了判断这些物品能否把背包装满的问题。

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

dp[j]:背包容量为j,其价值为dp[j]

2.确定递推公式

01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

本题物品i的重量是nums[i],价值也是nums[i]。

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

3.初始化

dp[0] = 0;        非零下标全部初始化为0,方便后面更改数据取最大值。

4.遍历顺序

先物品再背包,一维背包压缩了数组,不能颠倒两个for循环。

背包后序遍历,确保每个物品只能使用一次。

可以尝试打印一下正序遍历的dp数组,物品就不止使用一次了。

正序遍历背包的话,会出现:

数组[1, 5, 11, 5],dp[1] = 1, dp[2] = 2,说明此时1使用了两次,不符合题目要求的一个物品只能使用一次。

class Solution {
    public boolean canPartition(int[] nums) {
        if(nums == null || nums.length == 0) {
            return false;
        }
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        //总和为奇数,不能平分
        if(sum % 2 != 0) {
            return false;
        }
        int target = sum / 2;
        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]);
            }
            if (dp[target] == target){
                return true;
            }
        }
        return dp[target] == target;
    }
}

你可能感兴趣的:(算法,java,leetcode,数据结构,开发语言)