代码随想录算法day30 | 动态规划算法part03 | 01背包问题 二维 ,01背包问题 一维,416. 分割等和子集

ops/2024/9/20 1:19:00/ 标签: 算法, 动态规划, leetcode, 数据结构, java

正式开始背包问题,背包问题还是挺难的,虽然大家可能看了很多背包问题模板代码,感觉挺简单,但基本理解的都不够深入。

背包问题,力扣上没有原题,题目是在卡码网找的

动态规划:01背包理论基础

本题力扣上没有原题,大家可以去卡码网第46题 (opens new window)去练习,题意是一样的。

正式开始讲解背包问题!

对于面试的话,其实掌握01背包完全背包,就够用了,最多可以再来一个多重背包

如果这几种背包,分不清,我这里画了一个图,如下:

416.分割等和子集1

除此以外其他类型的背包,面试几乎不会问,都是竞赛级别的了,leetcode上连多重背包的题目都没有,所以题库也告诉我们,01背包和完全背包就够用了。

而完全背包又是也是01背包稍作变化而来,即:完全背包的物品数量是无限的

所以背包问题的理论基础重中之重是01背包,一定要理解透

leetcode上没有纯01背包的问题,都是01背包应用方面的题目,也就是需要转化为01背包问题。

所以我先通过纯01背包问题,把01背包原理讲清楚,后续再讲解leetcode题目的时候,重点就是讲解如何转化为01背包问题了

01 背包

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

<a class=动态规划-背包问题" height="380" src="https://img-blog.csdnimg.cn/img_convert/0d2aa9d298345da090070774d9603bee.jpeg" width="450" />

这是标准的背包问题,以至于很多同学看了这个自然就会想到背包,甚至都不知道暴力的解法应该怎么解了。

这样其实是没有从底向上去思考,而是习惯性想到了背包,那么暴力的解法应该是怎么样的呢?

每一件物品其实只有两个状态,取 或者 不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是 O(2^n),这里的 n 表示物品数量。

所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!

在下面的讲解中,我举一个例子:

背包最大重量为4。

物品为:

重量价值
物品0115
物品1320
物品2430

问背包能背的物品最大价值是多少?

以下讲解和图示中出现的数字都是以这个例子为例。

(为了方便表述,下面描述 统一用 容量为XX的背包,放下容量(重量)为XX的物品,物品的价值是XX)

二维dp数组01背包

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

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

我们需要使用二维数组,为什么呢?

因为有两个维度需要分别表示:物品 和 背包容量

如图,二维数组为 dp[i][j]。

<a class=动态规划-背包问题1" height="494" src="https://img-blog.csdnimg.cn/img_convert/f6566fee510e11dc7a4c278df816897a.png" width="912" />

那么这里 i 、j、dp[i][j] 分别表示什么呢?

i 来表示物品、j表示背包容量

(如果想用 j 表示物品,i 表示背包容量 行不行? 都可以的,个人习惯而已)

我们来尝试把上面的 二维表格填写一下。

动态规划的思路是根据子问题的求解推导出整体的最优解

我们先看把物品0 放入背包的情况:

背包容量为0,放不下物品0,此时背包里的价值为0。

背包容量为1,可以放下物品0,此时背包里的价值为15.

背包容量为2,依然可以放下物品0 (注意 01背包里物品只有一个),此时背包里的价值为15。

以此类推。

再看把物品1 放入背包:

背包容量为 0,放不下物品 0 或者物品 1,此时背包里的价值为 0。

背包容量为 1,只能放下物品0,背包里的价值为15。

背包容量为 2,只能放下物品0,背包里的价值为15。

背包容量为 3,上一行同一状态,背包只能放物品 0,这次也可以选择物品1了,背包可以放物品1 或者 物品0,物品1价值更大,背包里的价值为20。

背包容量为 4,上一行同一状态,背包只能放物品 0,这次也可以选择物品 1 了,背包可以放下物品 0 和 物品 1,背包价值为 35。

以上举例,是比较容易看懂,我主要是通过这个例子,来帮助大家明确 dp数组 的含义。

上图中,我们看 dp[1][4] 表示什么意思呢。

任取 物品0,物品1 放进容量为4的背包里,最大价值是 dp[1][4]。

通过这个举例,我们来进一步明确 dp数组 的含义。

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

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

  • 确定递推公式

这里在把基本信息给出来:

重量价值
物品0115
物品1320
物品2430

对于递推公式,首先我们要明确有哪些方向可以推导出 dp[i][j]。

这里我们 dp[1][4] 的状态来举例:

求取 dp[1][4] 有两种情况:

  1. 放 物品1
  2. 还是 不放物品1

如果 不放物品1, 那么背包的价值应该是 dp[0][4] 即 容量为4的背包,只放物品0的情况。

推导方向如图:

如果放物品1, 那么背包要先留出物品1的容量,目前容量是4,物品1 的容量(就是物品1的重量)为3,此时背包剩下容量为1。

容量为1,只考虑放物品0 的最大价值是 dp[0][1],这个值我们之前就计算过。

所以 放物品1 的情况 = dp[0][1] + 物品1 的价值,推导方向如图:

两种情况,分别是放物品1 和 不放物品1,我们要取最大值(毕竟求的是最大价值)

dp[1][4] = max(dp[0][4], dp[0][1] + 物品1 的价值)

以上过程,抽象化如下:

  • 不放物品 i:背包容量为 j,里面不放物品 i 的最大价值是dp[i - 1][j]。

  • 放物品i:背包空出物品 i 的容量后,背包容量为 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。如图:

<a class=动态规划-背包问题2" height="498" src="https://img-blog.csdnimg.cn/img_convert/760a26da9cae3abcb6067ed85399b46a.png" width="852" />

再看其他情况。

状态转移方程 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 物品。

代码初始化如下:

java">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数组初始化情况如图所示:

<a class=动态规划-背包问题7" height="456" src="https://img-blog.csdnimg.cn/img_convert/44ada85f4f364dea970251af923bd672.png" width="838" />

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,更方便一些。

如图:

<a class=动态规划-背包问题10" height="466" src="https://img-blog.csdnimg.cn/img_convert/ce54f7d42daddc51e9b8be559c34fc9f.jpeg" width="804" />

最后初始化代码如下:

java">// 初始化 dp
int[][] dp = new int[n][bagweight + 1];
for (int j = weight[0]; j <= bagweight; j++) {dp[0][j] = value[0];
}

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

  • 确定遍历顺序

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

<a class=动态规划-背包问题3" height="466" src="https://img-blog.csdnimg.cn/img_convert/02f31284344d576aff284939493c6db3.png" width="866" />

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

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

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

java">// weight 数组的大小 就是 物品个数
for(int i = 1; i < weight.length(); 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数组)

例如这样:

java">// weight 数组的大小 就是物品个数
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量for(int i = 1; i < weight.length(); 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] 的左上角方向(包括正上方向),那么先遍历物品,再遍历背包的过程如图所示:

<a class=动态规划-背包问题5" height="492" src="https://img-blog.csdnimg.cn/img_convert/3668feb1e7e719a263ddd7baf69afb6f.png" width="892" />

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

<a class=动态规划-背包问题6" height="484" src="https://img-blog.csdnimg.cn/img_convert/c313c235dfb2db210043a32db37afe9e.png" width="850" />

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

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

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

  • 举例推导dp数组

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

<a class=动态规划-背包问题4" height="440" src="https://img-blog.csdnimg.cn/img_convert/9b807e2e91fde56bd631896d58495a1a.jpeg" width="850" />

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

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

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

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

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

本题力扣上没有原题,大家可以去卡码网第46题 (opens new window)去练习,题意是一样的,代码如下:

java">import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);int n = scanner.nextInt();int bagweight = scanner.nextInt();int[] weight = new int[n];int[] value = new int[n];for (int i = 0; i < n; ++i) {weight[i] = scanner.nextInt();}for (int j = 0; j < n; ++j) {value[j] = scanner.nextInt();}int[][] dp = new int[n][bagweight + 1];for (int j = weight[0]; j <= bagweight; j++) {dp[0][j] = value[0];}for (int i = 1; i < n; i++) {for (int j = 0; j <= bagweight; 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]);}}}System.out.println(dp[n - 1][bagweight]);}
}
总结

背包问题 是动态规划里的经典类型题目,大家要细细品味。

可能有的同学并没有注意到 初始化 和 遍历顺序的重要性,我们后面做力扣上背包面试题目的时候,大家就会感受出来了。

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

说一说滚动数组,其实在前面的题目中我们已经用到过滚动数组了,就是把二维dp降为一维dp

那么我们通过01背包,来彻底讲一讲滚动数组!

接下来还是用如下这个例子来进行讲解

背包最大重量为4。

物品为:

重量价值
物品0115
物品1320
物品2430

问背包能背的物品最大价值是多少?

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

在使用二维数组的时候,递推公式: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数组的递推公式为: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

一维dp数组,其实就是上一层 dp[i-1] 这一层 拷贝到 dp[i] 来。

所以在 上面递推公式的基础上,去掉 i 这个维度就好。

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

以下为分析:

dp[j] 为容量为 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,指定是取最大的,毕竟是求最大价值,

所以递归公式为:

java">dp[j] = Math.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数组遍历顺序

代码如下:

java">for(int i = 0; i < weight.length; i++) { // 遍历物品for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);}
}

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

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

为什么呢?

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

举一个例子:物品 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数组的背包在遍历顺序上和二维其实是有很大差异的!,这一点大家一定要注意。

  • 举例推导dp数组

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

<a class=动态规划-背包问题9" height="482" src="https://img-blog.csdnimg.cn/img_convert/e1fe8615398466f52ec9f1e95370376f.png" width="890" />

本题力扣上没有原题,大家可以去卡码网第46题 (opens new window)去练习,题意是一样的,代码如下:

java">import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);int n = scanner.nextInt();int bagweight = scanner.nextInt();int[] weight = new int[n];int[] value = new int[n];for (int i = 0; i < n; ++i) {weight[i] = scanner.nextInt();}for (int j = 0; j < n; ++j) {value[j] = scanner.nextInt();}int[] dp = new int[bagweight + 1];for(int i = 1; i < n; i++) { // 遍历物品for(int j = bagweight; j >= weight[i]; j--) { // 遍历背包容量dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);}}System.out.println(dp[bagweight]);}
}
总结

以上的讲解可以开发一道面试题目(毕竟力扣上没原题)。

要求先实现一个纯二维的01背包,如果写出来了,然后再问为什么两个for循环的嵌套顺序这么写?反过来写行不行?再讲一讲初始化的逻辑。

然后要求实现一个一维数组的01背包,最后再问,一维数组的01背包,两个for循环的顺序反过来写行不行?为什么?

注意以上问题都是在候选人把代码写出来的情况下才问的。

就是纯01背包的题目,都不用考01背包应用类的题目就可以看出候选人对算法的理解程度了。

相信大家读完这篇文章,应该对以上问题都有了答案!

此时01背包理论基础就讲完了,大家可以发现其实信息量还是挺大的。

不用再凭感觉或者记忆去写背包,而是有自己的思考,了解其本质,代码的方方面面都在自己的掌控之中。

即使代码没有通过,也会有自己的逻辑去debug,这样就思维清晰了。


416. 分割等和子集

本题是 01背包的应用类题目

力扣题目链接(opens new window)

题目难易:中等

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

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

示例 1:

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

示例 2:

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

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

这道题目初步看,和如下两题几乎是一样的,大家可以用回溯法,解决如下两题

  • 698.划分为k个相等的子集
  • 473.火柴拼正方形

这道题目是要找是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

那么只要找到集合里能够出现 sum / 2 的子集总和,就算是可以分割成两个相同元素和子集了。

本题是可以用回溯暴力搜索出所有答案的,但最后超时了,也不想再优化了,放弃回溯,直接上01背包吧。

01背包问题

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

背包问题有多种背包方式,常见的有:01背包、完全背包、多重背包、分组背包和混合背包等等。

要注意题目描述中商品是不是可以重复放入。

即一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包,写法还是不一样的。

要明确本题中我们要使用的是01背包,因为元素我们只能用一次。

回归主题:首先,本题要求集合里能否出现总和为 sum / 2 的子集。

那么来一一对应一下本题,看看背包问题如何来解决。

只有确定了如下四点,才能把01背包问题套到本题上来。

  • 背包的体积为sum / 2
  • 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为 元素的数值
  • 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
  • 背包中每一个元素是不可重复放入。

以上分析完,我们就可以套用01背包,来解决这个问题了。

动规五部曲分析如下:

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

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

本题中每一个元素的数值既是重量,也是价值。

套到本题,dp[j] 表示 背包总容量(所能装的总重量)是 j,放进物品后,背的最大重量为 dp[j]

那么如果背包容量为 target, dp[target] 就是装满背包之后的重量,所以当 dp[target] == target 的时候,背包就装满了。

拿输入数组 [1, 5, 11, 5],举例, dp[7] 只能等于 6,因为 只能放进 1 和 5。

而 dp[6] 就可以等于6了,放进1 和 5,那么dp[6] == 6,说明背包装满了。

  • 确定递推公式

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

  • dp数组如何初始化

在01背包,一维dp如何初始化,已经讲过,

从 dp[j] 的定义来看,首先 dp[0]一定是0。

如果题目给的价值都是正整数那么非 0 下标都初始化为 0 就可以了,如果题目给的价值有负数,那么非 0 下标就要初始化为负无穷

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

本题题目中 只包含正整数的非空数组,所以非0下标的元素初始化为0就可以了。

代码如下:

java">// 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200
// 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
int[] dp = new int[10001];
Arrays.fill(dp, 0);
  • 确定遍历顺序

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

代码如下:

java">// 开始 01背包
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]);}
}
  • 举例推导dp数组

dp[j] 的数值一定是小于等于 j 的。

如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j,理解这一点很重要。

用例1,输入[1,5,11,5] 为例,如图:

416.分割等和子集2

最后dp[11] == 11,说明可以将这个数组分割成两个子集,使得两个子集的元素和相等。

综上分析完毕,Java 代码如下:

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 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 < n; i++) {for(int j = target; j >= nums[i]; j--) {//物品 i 的重量是 nums[i],其价值也是 nums[i]dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);}//剪枝一下,每一次完成內層的for-loop,立即檢查是否dp[target] == target,優化時間複雜度(26ms -> 20ms)if(dp[target] == target)return true;}return dp[target] == target;}
}

http://www.ppmy.cn/ops/110909.html

相关文章

简单易懂的方式来解释机器学习(ML)和深度学习(DL)的区别与联系

让我们从多个角度出发&#xff0c;用一些简单易懂的方式来解释机器学习&#xff08;ML&#xff09;和深度学习&#xff08;DL&#xff09;的区别与联系。 1. 概念上的区别 机器学习 想象一下你在教一个小孩子如何分辨猫和狗。你可能会给这个孩子看很多猫和狗的照片&#xff…

Unity-Time类

目录 Time.timeScale Time.deltaTime Time.unscaledDeltaTime Time.time Time.frameCount Time.fixedDeltaTime Time.timeScale 时间缩放比例 时间停止 Time.timeScale 0; //回复正常 //Time.timeScale 1; //2倍速 …

VSCode 离线安装中文语言包

1.插件市场 Extensions for Visual Studio family of products | Visual Studio Marketplace 输入&#xff1a; language 在version history里面下载相应的版本&#xff0c;若没有就下载最新的 在下面安装 安装完重启就可以了。 可能会提示的失败&#xff1a; Unable to ins…

【vscode】 快速生成react组件

在VSCode中快速生成React组件的方法主要包括使用内置的代码片段和安装第三方插件。以下是具体的步骤和方法&#xff1a; 使用内置代码片段‌ VSCode支持用户自定义代码片段&#xff0c;你可以通过输入特定的前缀&#xff0c;然后按下Tab键&#xff0c;来快速生成React组件的基…

Cenos7镜像+Docker问题

前言 好用的镜像 阿里 curl -o /etc/yum.repos.d/Centos7-aliyun.repo https://mirrors.wlnmp.com/centos/Centos7-aliyun-x86_64.repo科大 curl -o /etc/yum.repos.d/Centos7-ustc.repo https://mirrors.wlnmp.com/centos/Centos7-ustc-x86_64.repo网易 curl -o /etc/yum…

概率论原理精解【14】

文章目录 拓扑空间拓扑空间映射一、连续映射二、同胚映射三、开映射与闭映射四、其他映射五、映射空间 第一可数空间第一可数性公理第一可数空间的性质例子与反例 邻域基定义性质例子应用 参考文献 拓扑空间 拓扑空间映射 是一个相当宽泛且复杂的话题&#xff0c;因为拓扑空间…

HW蓝队某知名大厂面试题分享

&#x1f91f; 基于入门网络安全/黑客打造的资源包无偿分享中&#xff1a; &#x1f449;黑客&网络安全入门&进阶学习资源包 应急响应流程 1&#xff09;首先判断服务器资产、影响范围以及严重程度&#xff0c;确认有没有必要将服务器下线隔离&#xff0c;然后根据服务…

基于python+django+mysql+Nanodet检测模型的水稻虫害检测系统

博主介绍&#xff1a; 大家好&#xff0c;本人精通Java、Python、C#、C、C编程语言&#xff0c;同时也熟练掌握微信小程序、Php和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我有丰富的成品Java、Python、C#毕设项目经验&#xff0c;能够为学生提供各类…

第158天:安全开发-Python-Socket编程反弹Shell分离免杀端口探针域名爆破

前置知识 使用 socket 模块 1. 导入模块 首先&#xff0c;你需要导入 Python 的 socket 模块。 import socket 2. 创建套接字 使用 socket.socket() 函数创建一个新的套接字。这个函数可以接收两个参数&#xff1a;地址族和套接字类型。 地址族&#xff08;Address Family&…

筑牢网络安全防线:为数字时代保驾护航

《筑牢网络安全防线&#xff1a;为数字时代保驾护航》 一、网络安全&#xff1a;数字时代的关键课题 网络安全在当今数字时代的重要性愈发凸显。2024 年国家网络安全宣传周以 “网络安全为人民&#xff0c;网络安全靠人民” 为主题&#xff0c;深刻体现了网络安全与每个人息息…

OpenGL(四) 纹理贴图

几何模型&材质&纹理 渲染一个物体需要&#xff1a; 几何模型&#xff1a;决定了物体的形状材质&#xff1a;绝对了当灯光照到上面时的作用效果纹理&#xff1a;决定了物体的外观 纹理对象 纹理有2D的&#xff0c;有3D的。2D图像就是一张图片&#xff0c;3D图像是在…

MyBatis 如何将 Mapper 接口与其 XML 映射文件关联:深入原理与实现

MyBatis 如何将 Mapper 接口与其 XML 映射文件关联&#xff1a;深入原理与实现 1. 概述 MyBatis 是一个简单、灵活的持久层框架&#xff0c;它通过 SQL 语句将 Java 对象与数据库进行映射。MyBatis 支持基于 XML 和注解的配置方式。在实际开发中&#xff0c;XML 映射文件与 M…

云服务与虚拟主机:数字时代的网络托管选择

当今数字化的时代&#xff0c;无论是个人博主、小型企业还是大型企业&#xff0c;都需要一个可靠的网络托管解决方案来展示自己的网站或应用程序。云服务和虚拟主机是两种常见的选择&#xff0c;它们各有特点&#xff0c;适用于不同的需求。 一、虚拟主机 虚拟主机&#xff0…

SpringBoot闲一品交易平台

SpringBoot闲一品交易平台 #vue项目实战 #计算机项目 #java项目 SpringBoot闲一品交易平台通过运用软件工程原理和开发方法&#xff0c;借助Spring Boot框架&#xff0c;旨在实现零食交易信息的高效管理&#xff0c;提升用户的购物体验和满意度。 技术栈 开发语言&#xff1a;…

想了解医疗大模型吗?请看《智能系统学报》实验室最新综述论文

本文改编自实验室的最新综述论文《医疗领域的大型语言模型综述》&#xff0c;该论文发表于《智能系统学报》。《智能系统学报》是中国人工智能学会会刊、“中国人工智能学会推荐中文学术期刊”列表中的A类期刊。该论文合作单位包括上海理工大学、上海儿童医学中心、复旦大学附属…

文件存储阿里云

1.图片存储 图片存储是指将图片文件保存在服务器或云存储中的技术或服务。图片存储的主要目的是方便用户上传、存储、管理和分享图片文件。 图片存储可以分为两种主要类型&#xff1a;本地存储和云存储。 本地存储是将图片文件保存在本地服务器或计算机上的一种方式。这种存…

服务网关Gateway快速入门

1.引入 网关可以把它理解成坐高铁时的安检&#xff0c;他可以对用户做身份验证&#xff0c;哪些人能通过&#xff0c;哪些人不能通过&#xff0c;都由他决定&#xff0c;如果没有安检&#xff0c;那么高铁的安全性将受到打击&#xff0c;一个微服务没有网关&#xff0c;那么接口…

使用Docker快速启动Nacos集群

Nacos 是一个易于使用的平台&#xff0c;用于动态服务发现、配置管理和服务管理。它帮助您在云环境中快速构建云原生应用程序&#xff0c;支持服务的注册与发现、动态配置更新等功能。在本文中&#xff0c;我们将介绍如何使用 Docker 快速启动 Nacos 集群。 为什么使用 Docker…

美国税收制度及SAP实施

1. 税制综述 美国是以直接税为主的国家,实行联邦、州和地方&#xff08;市、县&#xff09;三级征税制度&#xff0c;属于彻底的分税制国家。美国联邦税以个人所得税和企业所得税为其主要收入来源&#xff0c;州税以销售与使用税为其主要收入来源&#xff0c;地方税以财产税为…

Spring MVC的异步模式(ResponseBodyEmitter、SseEmitter、StreamingResponseBody)

在SpringBoot异步接口实现&#xff1a;提高系统的吞吐量中&#xff0c;讲到使用Callable、WebAsyncTask、DeferredResult来提供异步接口&#xff0c;但是他们仅用于返回单个异步值——即返回一个值之后&#xff0c;就不能再返回值了。如果想生成多个异步值并将这些值写入响应&a…