代码随想录 动态规划-0-1背包问题

news2024/11/16 7:45:44

目录

标准0-1背包问题

二维dp数组01背包

一维dp数组01背包(滚动数组)

416.分割等和子集

1049.最后一块石头的重量||

494.目标和

474.一和零 


背包问题的分类

标准0-1背包问题

46. 携带研究材料(第六期模拟笔试)

时间限制:5.000S  空间限制:128MB

题目描述

小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。 

小明的行李空间为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料只能选择一次,并且只有选与不选两种选择,不能进行切割。

输入描述

第一行包含两个正整数,第一个整数 M 代表研究材料的种类,第二个正整数 N,代表小明的行李空间。

第二行包含 M 个正整数,代表每种研究材料的所占空间。 

第三行包含 M 个正整数,代表每种研究材料的价值。

输出描述

输出一个整数,代表小明能够携带的研究材料的最大价值。

输入示例

6 1
2 2 3 1 5 2
2 3 1 5 4 3

输出示例

5

提示信息

小明能够携带 6 种研究材料,但是行李空间只有 1,而占用空间为 1 的研究材料价值为 5,所以最终答案输出 5。 

数据范围:
1 <= N <= 5000
1 <= M <= 5000
研究材料占用空间和价值都小于等于 1000

二维dp数组01背包

背包问题给出的物品和背包问题容量这两个参数,可以作为递推的规则

依然动规五部曲分析一波。

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

对于背包问题,有一种写法, 是使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

只看这个二维数组的定义,大家一定会有点懵,看下面这个图:

动态规划-背包问题1

要时刻记着这个dp数组的含义,下面的一些步骤都围绕这dp数组的含义进行的,如果哪里看懵了,就来回顾一下i代表什么,j又代表什么。

确定递推公式

再回顾一下dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

那么可以有两个方向推出来dp[i][j],

不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)

放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

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

dp数组如何初始化

关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱

首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。如图:

动态规划-背包问题2

在看其他情况。

状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。

dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。

当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。

代码初始化如下:

for (int j = 0 ; j < weight[0]; j++) {  // 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略,但很多同学应该没有想清楚这一点。
    dp[0][j] = 0;
}
// 正序遍历
for (int j = weight[0]; j <= bagweight; j++) {
    dp[0][j] = value[0];
}

此时dp数组初始化情况如图所示:

动态规划-背包问题7

dp[0][j] 和 dp[i][0] 都已经初始化了,那么其他下标应该初始化多少呢?

其实从递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出dp[i][j] 是由左上方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。

初始-1,初始-2,初始100,都可以!

但只不过一开始就统一把dp数组统一初始为0,更方便一些。

如图:

动态规划-背包问题10

最后初始化代码如下:

// 初始化 dp
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagweight; j++) {
    dp[0][j] = value[0];
}

费了这么大的功夫,才把如何初始化讲清楚,相信不少同学平时初始化dp数组是凭感觉来的,但有时候感觉是不靠谱的。

确定遍历顺序

在如下图中,可以看出,有两个遍历的维度:物品与背包重量

动态规划-背包问题3

那么问题来了,先遍历 物品还是先遍历背包重量呢?

其实都可以!! 但是先遍历物品更好理解

那么我先给出先遍历物品,然后遍历背包重量的代码。

// weight数组的大小 就是物品个数
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]);

    }
}

先遍历背包,再遍历物品,也是可以的!(注意我这里使用的二维dp数组)

例如这样:

// weight数组的大小 就是物品个数
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
    for(int i = 1; i < weight.size(); i++) { // 遍历物品
        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]);
    }
}

为什么也是可以的呢?

要理解递归的本质和递推的方向

dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 递归公式中可以看出dp[i][j]是靠dp[i-1][j]和dp[i - 1][j - weight[i]]推导出来的。

dp[i-1][j]和dp[i - 1][j - weight[i]] 都在dp[i][j]的左上角方向(包括正上方向),那么先遍历物品,再遍历背包的过程如图所示:

动态规划-背包问题5

再来看看先遍历背包,再遍历物品呢,如图:

动态规划-背包问题6

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

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

其实背包问题里,两个for循环的先后循序是非常有讲究的,理解遍历顺序其实比理解推导公式难多了

举例推导dp数组

来看一下对应的dp数组的数值,如图:

动态规划-背包问题4

最终结果就是dp[2][4]。

建议大家此时自己在纸上推导一遍,看看dp数组里每一个数值是不是这样的。

做动态规划的题目,最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下,然后在动手写代码!

很多同学做dp题目,遇到各种问题,然后凭感觉东改改西改改,怎么改都不对,或者稀里糊涂就改过了。

主要就是自己没有动手推导一下dp数组的演变过程,如果推导明白了,代码写出来就算有问题,只要把dp数组打印出来,对比一下和自己推导的有什么差异,很快就可以发现问题了。

import java.util.*;  
  
public class Main {  
  
    public static void main(String[] args) {  
        // 初始化一个Scanner对象用于读取用户输入  
        Scanner sc = new Scanner(System.in);  
  
        // 读取背包的容量N和物品种类M  
        int M = sc.nextInt();  // 物品种类  
        int N = sc.nextInt();  // 背包容量  
  
        // 初始化两个数组,分别用于存储物品的价值和重量  
        int[] values = new int[M];  
        int[] weights = new int[M];  
  
        // 读取每种物品的重量  
        for(int i = 0; i < M; i++) {  
            weights[i] = sc.nextInt();  
        }  
  
        // 读取每种物品的价值  
        for(int i = 0; i < M; i++) {  
            values[i] = sc.nextInt();  
        }  
  
        // 初始化动态规划数组dp,其中dp[i][j]表示前i个物品放入容量为j的背包中所能获得的最大价值 
        //容量可以为0,故这里定义为N + 1
        int[][] dp = new int[M][N + 1];  
  
        // 对于第一个物品,如果其重量小于等于当前背包容量,则直接将其价值赋给dp数组对应位置  
        for(int i = weights[0]; i <= N; i++) {  
            dp[0][i] = values[0];  
        }  
  
        // 动态规划过程  
        for(int i = 1; i < dp.length; i++){  // 遍历所有物品  
            for(int j = 1; j < dp[0].length; j++){  // 遍历所有背包容量  
                // 如果当前物品重量大于背包容量,则当前背包无法装入该物品,价值与除去这个物品的情况一样 
                if(weights[i] > j) {  
                    dp[i][j] = dp[i-1][j];  
                } else {  
                    // 如果当前物品重量小于等于背包容量,则比较放入和不放入该物品两种情况下的价值,取最大值
                    //放入的情况是除去当前物品,容量减少该物品能放入的最大价值加该物品的价值
                    dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weights[i]] + values[i]);  
                }  
            }  
        }  
  
        // 输出最大价值,即dp数组的最后一个元素  
        System.out.println(dp[M - 1][N]);  
    }  
}

一维dp数组01背包(滚动数组)

对于背包问题其实状态都是可以压缩的。

在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);

与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。

这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。

读到这里估计大家都忘了 dp[i][j]里的i和j表达的是什么了,i是物品,j是背包容量。

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

一定要时刻记住这里i和j的含义,要不然很容易看懵了。

动规五部曲分析如下:

确定dp数组的定义

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

一维dp数组的递推公式

dp[j]为 容量为j的背包所背的最大价值,那么如何推导dp[j]呢?

dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。

dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])

此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值,

所以递归公式为:

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

可以看出相对于二维dp数组的写法,就是把dp[i][j]中i的维度去掉了。

一维dp数组如何初始化

关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱

dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。

那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?

看一下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。

这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了

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

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

    }
}

这里大家发现和二维dp的写法中,遍历背包的顺序是不一样的!

二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。

为什么呢?

倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
通过递归公式以及上边的分析来说,在原来的二维数组中dp[j]的值是由上方及左上方的值决定的,换到一维数组时,就由dp[j](上一层的值)和左侧上一层的值决定,如果正序遍历,则左边的值已经变成了本层的dp[j],所以要倒序遍历。

举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15

如果正序遍历

dp[1] = dp[1 - weight[0]] + value[0] = 15

dp[2] = dp[2 - weight[0]] + value[0] = 30

此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

为什么倒序遍历,就可以保证物品只放入一次呢?

倒序就是先算dp[2]

dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)

dp[1] = dp[1 - weight[0]] + value[0] = 15

所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

那么问题又来了,为什么二维dp数组遍历的时候不用倒序呢?

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

(如何这里读不懂,大家就要动手试一试了,空想还是不靠谱的,实践出真知!)

再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?

不可以!

因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。

倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。

(这里如果读不懂,就再回想一下dp[j]的定义,或者就把两个for循环顺序颠倒一下试试!)

所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的!,这一点大家一定要注意。

举例推导dp数组

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

动态规划-背包问题9

import java.util.*;  
  
public class Main {  
  
    public static void main(String[] args) {  
        // 创建一个Scanner对象,用于从控制台读取用户输入  
        Scanner sc = new Scanner(System.in);  
  
        // 从用户处读取背包的总容量N和物品种类数量M  
        int M = sc.nextInt();  // M表示物品种类的数量  
        int N = sc.nextInt();  // N表示背包的总容量  
  
        // 初始化两个数组,分别用于存储每种物品的价值和重量  
        int[] values = new int[M];  // 物品价值数组  
        int[] weights = new int[M]; // 物品重量数组  
  
        // 读取每种物品的重量,并存入weights数组  
        for (int i = 0; i < M; i++) {  
            weights[i] = sc.nextInt();  
        }  
  
        // 读取每种物品的价值,并存入values数组  
        for (int i = 0; i < M; i++) {  
            values[i] = sc.nextInt();  
        }  
  
        // 初始化动态规划数组dp,用于存储每个容量下能装入物品的最大价值  
        // 由于背包容量可以为0到N,故数组长度为N+1  
        int[] dp = new int[N + 1];  
  
        // 动态规划过程,从后往前遍历背包容量,确保每个物品只被考虑一次  
        for (int i = 0; i < M; i++) {  // 遍历每一种物品  
            for (int j = N; j >= weights[i]; j--) {  // 从最大容量开始遍历到当前物品重量  
                // 如果当前物品重量小于等于当前背包容量,则比较放入和不放入该物品两种情况下的价值  
                dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);  
            }  
        }  
  
        // 输出最大价值,即dp数组在容量为N时的值  
        System.out.println(dp[N]);  
  
        // 关闭Scanner对象,释放资源  
        sc.close();  
    }  
}

416.分割等和子集

416. 分割等和子集

中等

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

示例 1:

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

示例 2:

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

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100
class Solution {  
    public boolean canPartition(int[] nums) {  
        // 假设物品的重量和价值相等,因此这里直接使用原数组作为重量和价值数组  
        int[] weight = nums;  
        int[] value = nums;  
        int sum = 0;  
        // 计算数组总和  
        for(int i = 0; i < nums.length; i++){  
            sum += nums[i];  
        }  
        // 如果总和为奇数,则无法平均分割,直接返回false  
        if(sum % 2 == 1){  
            return false;  
        }  
        // 初始化动态规划数组dp,长度为总和的一半加1  
        // dp[j]表示容量为j的背包所能装下的最大价值  
        int dp[] = new int[sum/2 + 1];  
          
        // 遍历每个物品  
        for(int i = 0; i < nums.length;i++){  
            // 从背包的最大容量开始向前遍历  
            for(int j = dp.length - 1; j >= 0; j--){  
                // 如果当前背包容量j大于等于当前物品的重量weight[i]  
                if(j >= weight[i]){  
                    // 更新dp[j],比较当前物品放入背包和不放入背包两种情况下的最大价值  
                    dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);  
                }  
            }  
        }  
          
        // 如果dp数组中最大容量的价值等于总和的一半,说明可以将数组平均分割成两个子集,使得它们的和相等  
        if(dp[dp.length - 1] == sum/2){  
            return true;  
        }  
        // 否则无法平均分割  
        return false;  
    }  
}
class Solution {  
    public boolean canPartition(int[] nums) {  
        // 假设物品的重量和价值相等,因此这里直接使用原数组作为重量和价值数组  
        int[] weight = nums;  
        int[] values = nums;  
  
        // 获取数组长度  
        int len = nums.length;  
        // 如果数组为空,则无法分割成两个子集,直接返回false  
        if(len == 0){  
            return false;  
        }  
  
        // 计算数组的总和  
        int sum = 0;  
        for (int num : nums){  
            sum += num;  
        }  
          
        // 如果总和是奇数,则无法平均分割成两个子集,直接返回false  
        if(sum % 2 == 1){  
            return false;  
        }  
          
        // 目标值,即分割后每个子集应达到的和  
        int target = sum / 2;  
          
        // 初始化动态规划数组dp,dp[i][j]表示考虑前i个物品时,背包容量为j时的最大价值  
        int[][] dp = new int[nums.length][target + 1];  
  
        // 处理第一个物品  
        // 如果背包容量j大于等于第一个物品的重量,则放入第一个物品  
        for(int j = nums[0]; j <= target; j++){  
            dp[0][j] = nums[0];  
        }  
  
        // 处理剩余物品  
        for(int i = 1; i < len; i++){  
            for(int j = 0; j <= target; j++){  
                // 如果当前背包容量j小于当前物品的重量,则无法放入该物品,最大价值与前一个物品相同  
                if (j < nums[i]) {  
                    dp[i][j] = dp[i - 1][j];  
                } else {  
                    // 如果当前背包容量j大于等于当前物品的重量,则比较放入和不放入该物品两种情况下的最大价值  
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + values[i]);  
                }  
            }  
        }  
          
        // 如果考虑所有物品后,背包容量为target时的最大价值等于target,说明可以分割成两个和相等的子集  
        return dp[len - 1][target] == target;  
    }  
}

1049.最后一块石头的重量||

1049. 最后一块石头的重量 II

中等

提示

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0

示例 1:

输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

示例 2:

输入:stones = [31,26,33,21,40]
输出:5

提示:

  • 1 <= stones.length <= 30
  • 1 <= stones[i] <= 100

本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。(和分割等和子集相似)

本题物品的重量为stones[i],物品的价值也为stones[i]。

对应着01背包里的物品重量weight[i]和 物品价值value[i]。

接下来进行动规五步曲:

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

dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背最大重量为dp[j]

可以回忆一下01背包中,dp[j]的含义,容量为j的背包,最多可以装的价值为 dp[j]。

相对于 01背包,本题中,石头的重量是 stones[i],石头的价值也是 stones[i] ,可以 “最多可以装的价值为 dp[j]” == “最多可以背的重量为dp[j]”

确定递推公式

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

本题则是:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);

一些同学可能看到这dp[j - stones[i]] + stones[i]中 又有- stones[i] 又有+stones[i],看着有点晕乎。

大家可以再去看 dp[j]的含义。

dp数组如何初始化

既然 dp[j]中的j表示容量,那么最大容量(重量)是多少呢,就是所有石头的重量和。

把石头遍历一遍,计算出石头总重量 然后除2,得到dp数组的大小。

接下来就是如何初始化dp[j]呢,因为重量都不会是负数,所以dp[j]都初始化为0就可以了,这样在递归公式dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);中dp[j]才不会初始值所覆盖。

代码为:

vector<int> dp(15001, 0);

确定遍历顺序

如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

代码如下:

for (int i = 0; i < stones.size(); i++) { // 遍历物品
    for (int j = target; j >= stones[i]; j--) { // 遍历背包
        dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
    }
}

举例推导dp数组

举例,输入:[2,4,1,1],此时target = (2 + 4 + 1 + 1)/2 = 4 ,dp数组状态图如下:

1049.最后一块石头的重量II

最后dp[target]里是容量为target的背包所能背的最大重量。

那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。

在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的

那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。

class Solution {  
    public int lastStoneWeightII(int[] stones) {  
        // 计算所有石头的总重量  
        int sum = 0;  
        for (int i : stones) {  
            sum += i;  
        }  
        // 目标重量为总重量的一半,因为我们需要将石头分成尽可能接近的两堆  
        int target = sum >> 1;  
  
        // 初始化dp数组,dp[j]表示容量为j的背包所能装下的石头的最大重量  
        int[] dp = new int[target + 1];  
  
        // 遍历每块石头  
        for (int i = 0; i < stones.length; i++) {  
            // 逆序遍历目标重量,从大到小更新dp数组,保证每个石头只被使用一次  
            for (int j = target; j >= stones[i]; j--) {  
                // 对于当前石头,有两种选择:放入背包或不放入背包  
                // 放入背包:则背包的总重量为之前背包的总重量加上当前石头的重量  
                // 不放入背包:则背包的总重量不变  
                // 取两种情况中的较大值作为当前背包的总重量  
                dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);  
                //int[] weight = stones
                //int[] values = stones
            }  
        }  
  
        // 最后一堆石头的重量为总重量减去两堆中较大堆的重量(即dp[target]),因为dp[target]是两堆中较大堆的重量  
        // 所以,剩余一堆的重量为 sum - 2 * dp[target]  
        // 题目要求返回的是最后剩下的石头的重量,即两堆石头重量的差值  
        return sum - 2 * dp[target];  
    }  
}
class Solution {  
    public int lastStoneWeightII(int[] stones) {  
        // 计算所有石头的总重量  
        int sum = 0;  
        for (int s : stones) {  
            sum += s;  
        }  
  
        // 目标重量为总重量的一半,向下取整  
        int target = sum / 2;  
  
        // 初始化二维dp数组,dp[i][j]表示前i个物品放入容量为j的背包所能达到的最大重量  
        int[][] dp = new int[stones.length][target + 1];  
  
        // 对于第一个石头,初始化其对应的dp列  
        // 当背包容量j大于等于stones[0]时,可以放入该石头  
        for (int j = stones[0]; j <= target; j++) {  
            dp[0][j] = stones[0];  
        }  
  
        // 遍历每个石头  
        for (int i = 1; i < stones.length; i++) {  
            // 遍历背包的每个容量  
            for (int j = 1; j <= target; j++) {  
                // 如果当前背包容量j大于等于当前石头stones[i]的重量  
                if (j >= stones[i]) {  
                    // 选择放入当前石头,此时背包的重量为前i-1个物品在容量为j-stones[i]的背包中的最大重量加上当前石头的重量  
                    // 或者选择不放入当前石头,此时背包的重量为前i-1个物品在容量为j的背包中的最大重量  
                    // 取两种情况中的较大值作为当前背包的最大重量  
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - stones[i]] + stones[i]);  
                } else {  
                    // 如果当前背包容量j小于当前石头stones[i]的重量,则无法放入该石头,背包的最大重量与前i-1个物品在容量为j的背包中的最大重量相同  
                    dp[i][j] = dp[i - 1][j];  
                }  
            }  
        }  
  
        // 打印出背包的最大重量,便于调试(可选)  
        System.out.println(dp[stones.length - 1][target]);  
  
        // 两堆石头重量差值的最小值即为总重量减去两倍的背包最大重量  
        // 因为背包最大重量是使得两堆石头重量差值最小的那堆石头的重量  
        return sum - 2 * dp[stones.length - 1][target];  
    }  
}

494.目标和

494. 目标和

中等

给你一个非负整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 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
+1 + 1 + 1 + 1 - 1 = 3

示例 2:

输入:nums = [1], target = 1
输出:1

提示:

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 1000
  • 0 <= sum(nums[i]) <= 1000
  • -1000 <= target <= 1000

假设加法的总和为x,那么减法对应的总和就是sum - x。

所以我们要求的是 x - (sum - x) = target

x = (target + sum) / 2

此时问题就转化为,装满容量为x的背包,有几种方法

这里的x,就是bagSize,也就是我们后面要求的背包容量。

大家看到(target + sum) / 2 应该担心计算的过程中向下取整有没有影响。

这么担心就对了,例如sum 是5,S是2的话其实就是无解的,所以:

(C++代码中,输入的S 就是题目描述的 target)
if ((S + sum) % 2 == 1) return 0; // 此时没有方案

同时如果 S的绝对值已经大于sum,那么也是没有方案的。

(C++代码中,输入的S 就是题目描述的 target)
if (abs(S) > sum) return 0; // 此时没有方案

再回归到01背包问题,为什么是01背包呢?

因为每个物品(题目中的1)只用一次!

这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。

本题则是装满有几种方法。其实这就是一个组合问题了。

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

dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法

其实也可以使用二维dp数组来求解本题,dp[i][j]:使用 下标为[0, i]的nums[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法。

下面我都是统一使用一维数组进行讲解, 二维降为一维(滚动数组),其实就是上一层拷贝下来

确定递推公式

有哪些来源可以推出dp[j]呢?

只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。

例如:dp[j],j 为5,

已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。

已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。

已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 容量为5的背包

已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 容量为5的背包

已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包

那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。

所以求组合类问题的公式,都是类似这种:

dp[j] += dp[j - nums[i]]

这个公式在后面在讲解背包解决排列组合问题的时候还会用到!

dp数组如何初始化

这里看做组合问题,如果背包容量为1的话,那么只有一种方式就是往背包里什么都不放,所以dp[0] = 1;

确定遍历顺序

对于01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。

举例推导dp数组

输入:nums: [1, 1, 1, 1, 1], S: 3

bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4

dp数组状态变化如下:

class Solution {  
    // 公共方法,用于计算数组中和为目标值的组合数  
    public int findTargetSumWays(int[] nums, int target) {  
        //left是加和,right是减和
        //left - right = target
        //left + right = sum
        //right = sum - left
        //left = (target + sum) / 2
        // 计算数组的总和  
        int sum = 0;  
        for(int i = 0; i < nums.length; i++){  
            sum += nums[i];  
        }  
  
        // 如果目标值的绝对值大于数组的总和,那么不可能有任何组合的和等于目标值  
        if(Math.abs(target) > sum){  
            return 0;  
        }  
  
        // 如果目标值与数组总和之和是奇数,那么也不可能有任何组合的和等于目标值(因为和的一半必须是整数)  
        if((target + sum) % 2 == 1){  
            return 0;  
        }  
  
        // 计算目标值与数组总和之和的一半,这就是我们要在数组中找出其组合和为多少的数值  
        int size = (target + sum) / 2;  
  
        // 初始化动态规划数组,dp[i]表示和为i的组合数  
        int dp[] = new int[size + 1];  
        // 初始条件:和为0的组合有一种,即不选任何数字  
        dp[0] = 1;  
  
        // 遍历数组中的每个数字  
        for(int i = 0; i < nums.length; i++){  
            // 从大到小遍历可能的和,这样可以保证在计算dp[j]时,dp[j - nums[i]]的值是已经计算好的  
            for(int j = size; j >= nums[i]; j--){  
                //j >= nums[i]的时候下边的式子才有意义
                // dp[j]的值等于它自身(不选当前数字)与dp[j - nums[i]](选择当前数字)的和  
                dp[j] += dp[j - nums[i]];  
            }  
        }  
  
        // 返回和为size的组合数,即目标值与数组总和之和的一半的组合数  
        return dp[size];  
    }  
}

474.一和零 

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

本题中strs 数组里的元素就是物品,每个物品都是一个!

而m 和 n相当于是一个背包,两个维度的背包

理解成多重背包的同学主要是把m和n混淆为物品了,感觉这是不同数量的物品,所以以为是多重背包。

但本题其实是01背包问题!

只不过这个背包有两个维度,一个是m 一个是n,而不同长度的字符串就是不同大小的待装物品。

开始动规五部曲:

确定dp数组(dp table)以及下标的含义

dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]

确定递推公式

dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。

dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。

然后我们在遍历的过程中,取dp[i][j]的最大值。

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

此时大家可以回想一下01背包的递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

对比一下就会发现,字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。

这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。

dp数组如何初始化

01背包的dp数组初始化为0就可以。

因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。

确定遍历顺序

外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历!

那么本题也是,物品就是strs里的字符串,背包容量就是题目描述中的m和n。

代码如下:

for (string str : strs) { // 遍历物品
    int oneNum = 0, zeroNum = 0;
    for (char c : str) {
        if (c == '0') zeroNum++;
        else oneNum++;
    }
    for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历!
        for (int j = n; j >= oneNum; j--) {
            dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
        }
    }
}

有同学可能想,那个遍历背包容量的两层for循环先后循序有没有什么讲究?

没讲究,都是物品重量的一个维度,先遍历哪个都行!

举例推导dp数组

以输入:["10","0001","111001","1","0"],m = 3,n = 3为例

最后dp数组的状态如下所示:

474.一和零

class Solution {  
    // 定义一个公共方法,用于找到最大形式的字符串数量  
    public int findMaxForm(String[] strs, int m, int n) {  
        // 初始化一个二维动态规划数组dp,其维度为(m+1)x(n+1)。  
        // dp[i][j]表示容量为i个'0'和j个'1'的子集中可以形成的最大字符串数。  
        int[][] dp = new int[m + 1][n + 1];  
  
        // 遍历字符串数组中的每一个字符串  
        for(String s : strs){  
            // 初始化计数器,用于统计当前字符串中'0'和'1'的数量  
            int zeroNum = 0;  
            int oneNum = 0;  
  
            // 遍历当前字符串的每一个字符  
            for(int k = 0; k < s.length(); k ++){  
                // 获取当前字符  
                char a = s.charAt(k);  
  
                // 根据字符类型增加对应的计数器  
                if(a == '0'){  
                    zeroNum++;  
                }else{  
                    oneNum++;  
                }  
            }  
  
            // 从最大容量开始递减,比该字符串中的0和1的范围大才有更新的意义  
            for(int i = m; i >= zeroNum; i--){  
                for(int j = n; j >= oneNum; j--){  
                    // 更新dp数组,比较当前状态与添加当前字符串后的状态哪个更优  
                    dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);  
                }  
            }  
        }  
  
        // 返回最终结果,即在容量为m个'0'和n个'1'的情况下可以形成的最大字符串数  
        return dp[m][n];  
    }  
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1526817.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

超越传统的极限:解密B树与B+树的数据结构之美!

超越传统的极限&#xff1a;解密B树与B树的数据结构之美&#xff01; B树和B树是在计算机科学中常用的平衡查找树数据结构&#xff0c;它们在处理大规模数据和磁盘存储方面具有重要的优势。本文将深入介绍B树和B树的基本概念、特点以及它们在数据库和文件系统中的应用&#xff…

【德语常识】分类单词

【德语常识】分类单词 一&#xff0c;Colors二&#xff0c;Countries & Languages三&#xff0c; 一&#xff0c;Colors 二&#xff0c;Countries & Languages 三&#xff0c;

JNDI注入原理及利用IDEA漏洞复现

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【Java、PHP】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收…

VMware虚拟机Centos7硬盘扩容详细图文教程

这里写自定义目录标题 设置扩容空间容量10G为列子开机后输入df -h 查看磁盘空间运行fdisk -l&#xff0c;查看硬盘信息运行fdisk /dev/sda输入m&#xff0c;查看n为add new partition&#xff0c;输入n输入p &#xff0c;p之后的东西都选择为默认再输入t&#xff0c;分区号根据…

洛谷-P1449 后缀表达式

目录 何为后缀表达式&#xff1f; 模拟过程 AC代码 采用STL的stack 题目链接&#xff1a;P1449 后缀表达式 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 何为后缀表达式&#xff1f; 那后缀表达式是怎么算的呢 那显然就需要引用最开始说的栈了 因为后缀表表达式本来就是栈…

HTML5+CSS3+JS小实例:全屏范围滑块

实例:全屏范围滑块 技术栈:HTML+CSS+JS 效果: 源码: 【HTML】 <!DOCTYPE html> <html lang="zh-CN"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale…

零延迟轻量级网站指纹防御

文章信息 论文题目&#xff1a;Zero-delay Lightweight Defenses against Website Fingerprinting 期刊&#xff08;会议&#xff09;&#xff1a; 29th USENIX Security Symposium 时间&#xff1a;2020 级别&#xff1a;CCF A 文章链接&#xff1a;https://www.usenix.org/s…

PHP反序列化---字符串逃逸(增加/减少)

一、PHP反序列化逃逸--增加&#xff1a; 首先分析源码&#xff1a; <?php highlight_file(__FILE__); error_reporting(0); class A{public $v1 ls;public $v2 123;public function __construct($arga,$argc){$this->v1 $arga;$this->v2 $argc;} } $a $_GET[v…

一文了解如何做全基因集GSEA富集分析

原文链接:一文完成全基因集GSEA富集分析 本期内容 写在前面 我们前面分享过一文掌握单基因GSEA富集分析的教程,主要使用单基因的角度进行GSEA富集分析。 我们社群的同学咨询,全基因集的GSEA如何分析呢??其实,原理都是大同小异的,那么今天我们就简单的整理一下吧。 若…

PyTorch学习笔记之激活函数篇(二)

文章目录 2、Tanh函数2.1 公式2.2 对应的图像2.3 对应生成图像代码2.4 优点与不足2.5 torch.tanh()函数 2、Tanh函数 2.1 公式 Tanh函数的公式&#xff1a; f ( x ) e x − e − x e x e − x f(x)\frac{e^x-e^{-x}}{e^xe^{-x}} f(x)exe−xex−e−x​ Tanh函数的导函数&am…

Java实现定时发送邮件(基于Springboot工程)

1、功能概述&#xff1f; 1、在企业中有很多需要定时提醒的任务&#xff1a;如每天下午四点钟给第二天的值班人员发送值班消息&#xff1f;如提前一天给参与第二天会议的人员发送参会消息等。 2、这种定时提醒有很多方式如短信提醒、站内提醒等邮件提醒是其中较为方便且廉价的…

旅行社旅游线路预定管理系统asp.net

旅行社旅游线路预定管理系统 首页 国内游 境外游 旅游景点 新闻资讯 酒店信息―留言板 后台管理 后台管理导航菜单系统管理修改密码留言管理注册会员管理基础数据设置国别设置有份设地区设置 旅行社管理友情链接管理添加友情链接友情链接管理新闻资讯管理添加新闻资讯新闻资讯管…

10|代理(中):AgentExecutor究竟是怎样驱动模型和工具完成任务的?

LangChain 中的“代理”和“链”的差异究竟是什么&#xff1f;在链中&#xff0c;一系列操作被硬编码&#xff08;在代码中&#xff09;。在代理中&#xff0c;语言模型被用作推理引 擎来确定要采取哪些操作以及按什么顺序执行这些操作。 Agent 的关键组件 代理&#xff08…

静态综合实验

一&#xff0c;1.搭建拓扑结构并启动。 2.根据题意得该图需要14个网段&#xff0c;根据192.168.1.0/24划分子网段&#xff0c;如下&#xff1a; 划分完如图所示&#xff1a; 二、配置IP地址 R1路由器&#xff1a; 1.进入系统视图并改名. 2.接口配置IP地址&#xff1a…

Linux系统中安装Docker

用Linux既可以用虚拟机操作&#xff0c;也可以自己弄一个云服务器进行操作&#xff0c;两者没啥区别。 操作之前需要将用户等级升级到root级别&#xff0c;防止有些操作因为权限限制而不能操作。 安装docker非常简单&#xff0c;只需要一直按照下列命令顺序进行操作即可。 安装…

选Gitee还是GitHub?

2024年3月18日&#xff0c;周一晚上 我选择GitHub 因为GitHub可以无限创建仓库

clipboard好用的复制剪切库

clipboard是现代复制到剪贴板的工具&#xff0c;其 gzip 压缩后只有 3kb&#xff0c;能够减少选择文本的重复操作&#xff0c;点击按钮就可以复制指定内容&#xff0c;支持原生HTMLjs&#xff0c;vue3和vue2。使用方法参照官方文档&#xff0c;so easy&#xff01;&#xff01;…

数据结构(四)——串的模式匹配

4.2 串的模式匹配 4.2.1_朴素模式匹配算法 字符串模式匹配&#xff1a;在主串中找到与模式串相同的⼦串&#xff0c;并返回其所在位置 主串⻓度为n&#xff0c;模式串⻓度为 m 朴素模式匹配算法&#xff1a;将主串中所有⻓度为m的⼦串依次与模式串对⽐&#xff0c;直到找到⼀…

U盘变本地磁盘?数据恢复有妙招!

一、U盘异变本地磁盘现象 在日常工作和生活中&#xff0c;U盘作为便携式的存储设备&#xff0c;广受大家喜爱。然而&#xff0c;有时我们会遇到一个奇怪的现象&#xff1a;原本应该显示为可移动磁盘的U盘&#xff0c;在插入电脑后却变成了“本地磁盘”。这种情况不仅让人困惑&…

Android系统签名的制作与使用

目录 1. &#x1f4c2; 背景 2. &#x1f531; 制作Android系统签名 步骤一&#xff1a;找到platform.pk8和platform.x509.pem签名文件 步骤二&#xff1a;下载keytool-importkeypair签名工具 步骤三&#xff1a;使用签名文件和签名工具生成.jks签名文件 3. ✅ 使用Andro…