动态规划第七篇:01背包问题(目标和 + 一和零)

353 阅读7分钟

文章目录

494. 目标和

问题描述

给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例:

输入:nums: [1, 1, 1, 1, 1], S: 3 输出:5 解释:

-1+1+1+1+1 = 3 +1-1+1+1+1 = 3 +1+1-1+1+1 = 3 +1+1+1-1+1 = 3 +1+1+1+1-1 = 3

一共有5种方法让最终目标和为3。

提示:

数组非空,且长度不会超过 20 。
初始的数组的和不会超过 1000 。
保证返回的最终结果能被 32 位整数存下。

抽象成01背包问题

本题要如何使表达式结果为target,

既然为target,那么就一定有 left组合 - right组合 = target。

left + right等于sum,而sum是固定的。

公式来了, left - (sum - left) = target -> left = (target + sum)/2 。

target是固定的,sum是固定的,left就可以求出来。

此时问题就是在集合nums中找出和为left的组合。

在这里插入图片描述

动态规划01背包问题

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
       int sum =0;
       for (int i=0;i<nums.length;i++)
           sum = sum + nums[i];
        if (target > sum ) return 0;
        if (( target + sum )%2==1) return 0;
        int bigSize = ( target + sum )/2;
        int[] dp=new int[bigSize+1];  // int数组初始化全部为0  为什么数组长度为 bigSize+1,因为内循环变量的时候  int j=bigSize;i>=0;   一共 [0,bigSize] bigSize+1 个
        dp[0]=1;  // 这个是迭代的基础,对于计算总数的时候
        for(int i=0;i<nums.length;i++){
            for (int j=bigSize;j>=nums[i];j--) { // 虽然j-- 很笨,但是只能这么用  不断减少背包容量
                dp[j] = dp[j] + dp[j-nums[i]]; // j-nums[i] 不能数组越界  计算总数的都是这个dp状态方程  
            }
        }
        return dp[bigSize];  // dp长度为 bigSize+1 ,这就是最后一个元素
    }
}

一维数组改成二维数组
1、新增第一个维度是 物体 数量; 先dp[i][0]=1业务初始化,而后才是二维dp数组的模板初始化 i=0
2、对于 i=0 初始化 初始化要按照一维数组的条件来,是 for (int j = bigSize; j >= nums[0]; j--) ,也可以是 for (int j = bigSize; j >= 0; j--) if...else... 一个意思;
3、第一个for循环 i 从1开始,第二个for循环可以反向,也可以正向
4、循环内部要if else 分别处理 状态方程第一维,是 dp[i][j] = dp[i-1][j] + dp[i-1][j - nums[i]]; 不是 dp[i][j] = dp[i][j] + dp[i][j - nums[i]];
5、最后返回,第一个维度为 物体数量 最后一个,第二个维度表示 容量 target

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++)
            sum = sum + nums[i];
        if (target > sum) return 0;
        if ((target + sum) % 2 == 1) return 0;
        int bigSize = (target + sum) / 2;  // 得到背包重量容量   背包价值就是target,不用管了,只要管好背包重量容量就好
        int[][] dp = new int[nums.length][bigSize + 1];  // int数组初始化全部为0  为什么数组长度为 bigSize+1,因为内循环变量的时候  int j=bigSize;i>=0;   一共 [0,bigSize] bigSize+1 个

        // 取代  dp[0]=1;  
        for (int i = 0; i < nums.length; i++)
            dp[i][0] = 1;

        // 初始化第一行不是取代 dp[0]=1;  这个是迭代的基础,对于计算总数的时候  装满容量为0的背包,有1种方法,就是装0件物品
        for (int j = bigSize; j >= nums[0]; j--) {
            dp[0][j] = dp[0][j] + dp[0][j - nums[0]]; // j-nums[i] 不能数组越界  计算总数的都是这个dp状态方程
        }

        for (int i = 1; i < nums.length; i++) {
            for (int j = bigSize; j >= 0; j--) { // 虽然j-- 很笨,但是只能这么用  不断减少背包容量
                if (j >= nums[i])
                    dp[i][j] = dp[i-1][j] + dp[i-1][j - nums[i]]; // j-nums[i] 不能数组越界  计算总数的都是这个dp状态方程
                else
                    dp[i][j] = dp[i-1][j];  // 必须用上一行控制下一行
            }
        }
        return dp[nums.length - 1][bigSize];  // dp长度为 bigSize+1 ,这就是最后一个元素
    }
}
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++)
            sum = sum + nums[i];
        if (target > sum) return 0;
        if ((target + sum) % 2 == 1) return 0;
        int bigSize = (target + sum) / 2;  // 得到背包重量容量   背包价值就是target,不用管了,只要管好背包重量容量就好
        int[][] dp = new int[nums.length][bigSize + 1];  // int数组初始化全部为0  为什么数组长度为 bigSize+1,因为内循环变量的时候  int j=bigSize;i>=0;   一共 [0,bigSize] bigSize+1 个


        for (int i = 0; i < nums.length; i++)
            dp[i][0] = 1;

        // 初始化第一行不是取代 dp[0]=1;  这个是迭代的基础,对于计算总数的时候  装满容量为0的背包,有1种方法,就是装0件物品
        for (int j = bigSize; j >= 0; j--) {
            if (j >= nums[0])
                dp[0][j] = dp[0][j] + dp[0][j - nums[0]]; // j-nums[i] 不能数组越界  计算总数的都是这个dp状态方程
            else
                dp[0][j] = dp[0][j];
        }

        for (int i = 1; i < nums.length; i++) {
            for (int j = bigSize; j >= 0; j--) { // 虽然j-- 很笨,但是只能这么用  不断减少背包容量
                if (j >= nums[i])
                    dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]; // j-nums[i] 不能数组越界  计算总数的都是这个dp状态方程
                else
                    dp[i][j] = dp[i - 1][j];  // 必须用上一行控制下一行
            }
        }
        return dp[nums.length - 1][bigSize];  // dp长度为 bigSize+1 ,这就是最后一个元素
    }
}

问题1:一维dp数组的含义和二维dp数组的含义?
回答1:
一维dp数组:填满j(包括j)这么大容积的包,有dp[i]种方法;
二维dp数组:使用 下标为[0~i]的 nums[i] 能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法。

问题2:dp状态方程的理解?
回答2:
一维:dp[j] = dp[j] + dp[j-nums[i]]; 对于容量为j背包,之前的方法数是dp[j],现在来了num[i],所以容量数 是 原来的dp[j] (j 容量的背包中不放入num[i])+ dp[j-nums[i]] (j 容量的背包中放入num[i])。
二维:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]; 对于容量为j背包,之前的方法数是dp[i-1][j],现在来了num[i],所以容量数 是 原来的dp[i-1][j] (j 容量的背包中不放入num[i])+ dp[i-1][j-nums[i]] (j 容量的背包中放入num[i])。

问题3:dp[0]的初始化为1 ?
回答3:从递归公式可以看出,在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递归结果将都是0。dp[0] = 1,理论上也很好解释,装满容量为0的背包,有1种方法,就是装0件物品。dp[j]其他下标对应的数值应该初始化为0,从递归公式也可以看出,dp[j]要保证是0的初始值,才能正确的由dp[j - nums[i]]推导出来。

问题4:dp数组为什么设置为 bigSize+1 ?
回答4:因为内循环变量的时候 int j=bigSize;j>=0;j--,所以, 一共 [0,bigSize] bigSize+1 个。

问题5:bigSize的意思?
回答5:bigSize的意义就是背包重要容量,就是为正数的数字的数字和。

474.一和零

问题描述

给你一个二进制字符串数组 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

抽象成01背包问题

彻底理解这个题目,一个图就好了,如下:

在这里插入图片描述

问题1:二维dp数组的含义?
回答1:dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。

问题2:dp状态方程的理解?
回答2:
二维:dp[i][j]=Math.max(dp[i][j],dp[i-zeroNum][j-oneNum]+1);
两个维度都是从一个一个字符来判断,当前的结果数存放在dp[i][j]里面,对于新来的 zeroNum oneNum,有两个状态供选择,
状态1:当结果中不选取 zeroNum oneNum, 结果就是仍然是 dp[i][j]
状态2:当结果中选取 zeroNum oneNum ,结果为 dp[i-zeroNum][j-oneNum]+1
两个状态、两种情况中选择比较大的。
对比标志的状态方程 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 发现,字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。所以,这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。

问题3:dp数组为什么设置为 m+1 n+1 ?
回答3:因为遍历的时候要用到 [m,0] [n,0]

问题4:为什么两层循环要用到 [m,0] [n,0]
回答4:因为对于新来的字符串数组的字符串,要全部更新dp数组。

动态规划01背包问题

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] dp=new int[m+1][n+1];   // 全部初始化为0
        for (String str:strs) {
            char[] chars=str.toCharArray();
            int zeroNum=0;int oneNum=0;
            for (char ch:chars){
                if ('0'==ch)
                    zeroNum++;
                else
                    oneNum++;
            }
            for(int i=m;i>=zeroNum;i--){  // 遍历范围从 [m,0]  所以定义长度为m+1
                for (int j=n;j>=oneNum;j--){ // 遍历范围从 [n,0]  所以定义长度为n+1
                    dp[i][j]=Math.max(dp[i][j],dp[i-zeroNum][j-oneNum]+1);
                }
            }
        }
        return dp[m][n];
    }
}

换成二维数组,现在已经是二维数组了…

换成三维数组太复杂,不涉及。