文章目录
- Day 44
- 01. 完全背包基础
- <1> 完全背包的区别
- <2> 案例
- 02. 零钱兑换 II(No. 518)
- <1> 题目
- <2> 笔记
- <3> 代码
- 03. 组合总和 IV(No. 377)
- <1> 题目
- <2> 笔记
- <3> 代码
Day 44
01. 完全背包基础
<1> 完全背包的区别
前面学到的 01 背包的 滚动数组 遍历方法:
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]);
}
}
而 完全背包问题 的代码是这样的
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
代码写出来很容易看出一个是正序遍历,而一个是倒序遍历;这是建立在滚动数组的方法会 先将上一层的内容复制到本层,然后在上一层代码的基础上来进行操作。
<2> 案例
比如来看这个案例,背包容量为 4
物品编号 | weight | value |
---|---|---|
0 | 1 | 15 |
1 | 3 | 20 |
2 | 4 | 30 |
- 对于 01背包来说,因为一个物品 只能取一次,所以推导数组中的一个元素,依赖的是 上一层 也就是左上角的内容,是在没有取得这个物品的基础上去决定取还是不取。
- 而对于完全背包问题来说,因为每个物品可以取得多次,所以它以来的其实是 本层 的内容,是在取得这个物品的基础上继续去求得最优解。
这就决定了它们的层序遍历一个是从后往前一个是从前往后,因为要看推导每个元素依赖的部分是什么。
02. 零钱兑换 II(No. 518)
题目链接
代码随想录题解
<1> 题目
给你一个整数数组 coins
表示不同面额的硬币,另给一个整数 amount
表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0
。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 1:
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:
输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。
示例 3:
输入:amount = 10, coins = [10]
输出:1
提示:
1 <= coins.length <= 300
1 <= coins[i] <= 5000
coins
中的所有值 互不相同0 <= amount <= 5000
<2> 笔记
本题和昨天的 目标和(No. 494)类似,都是给定一个容量,问填满这个容量 有多少种方法;可以去看一下我的这一篇博客:
代码随想录刷题笔记 DAY 42 | 最后一块石头的重量 II No.1049 | 目标和 No.494 | 一和零 No.474
填满一个容量有多少种方法的递推公式为 dp[j] += dp[j - nums[i]]
对于做过那道题目的朋友这个递推公式应该不陌生;但如果没有做过这道题的话这里推导一下:
- 首先写出二维数组的递推公式:
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]
- 对于一个新的元素,有两种选择,取或者不取这个元素,如果不取这个元素的话得到的就是前面的情况
dp[i - 1][j]
如果要取这个元素,是要在j - nums[i]
的 基础 上去取的,但因为本题中要求的是有多少种方法,所以最终得到的结果就是将这两个求和。
接下来重点讨论一下本题的遍历顺序,卡哥在视频和题解里并没有对这一块举例,这里附上我自己的理解和案例。
本题是属于组合问题,即 1 2 1
和 1 1 2
代表的是 同样的 元素,先来看先遍历物品的情况:
写出代码来就是这样的:
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
dp[j] += dp[j - coins[i]];
}
}
因为这里这里是按照物品,从上往下取遍历的,推导一个元素依赖的是它的 左上角 的元素,而因为是遍历物品的原因,它的左上角是一定不会含有本元素,因为上层中不会出现 2
所以就不会出现 2 1
和 1 2
这种重复的情况。
这与其 dp
数组的 更新顺序 有关系:
这个数组是先 全部填充满 再去 一行一行 的更新,重点关注一下这个部分,然后接下来来看先遍历背包的情况,依然先给出代码:
for (int j = 0; j <= amount; j++) { // 遍历背包容量
for (int i = 0; i < coins.size(); i++) { // 遍历物品
if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
}
}
在先遍历背包的时候 dp
数组什么时候会被充满呢?答案是 遍历到最后一列的时候,它是先遍历完第一列,然后填充完 dp
数组的 第一个元素,然后继续遍历第二列、第三列. . . ,直到将 dp
数组填充满,对于这块一定要理解,那这会带来什么影响呢?现在来关注上面标黄色的元素:
它对应的 dp
数组的这个位置,接下来可能会有些混乱,大家看的时候注意一下 主语:
- 先来看黄色的上一行的那个元素,它刚开始遍历的时候为
0
,然后dp[j] += dp[j - coins[i]];
可以算出j - coins[i]
也就是4 - 1 = 3
此时要加上下标为3
的那个情况,而下标为3
的情况是会包含1 2
这个组合的,与这里的1
组合后就会形成一个1 2 1
的情况。 - 此时再来看黄色的那个元素,它的
j - coins[i]
为4 - 2 = 2
而这个下标为2
的部分会包含1 1
这个组合,最终组合完成之后得到的结果就是1 1 2
,是不是突然发现出现重复情况了? - 推导本元素虽然依赖的是上方的元素,但是 上方元素 在推导的过程中会依赖于前面的元素,但这个前面元素提前遍历到了本元素,因此出现了重复的情况。
所以先遍历背包求得的是 排列,而先遍历物品求得的是 组合
写出代码
<3> 代码
class Solution {
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1]; // 初始化 dp 数组
dp[0] = 1;
for (int i = 0; i < coins.length; i++) {
for (int j = coins[i]; j < dp.length; j++) {
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
}
03. 组合总和 IV(No. 377)
题目链接
代码随想录题解
<1> 题目
给你一个由 不同 整数组成的数组 nums
,和一个目标整数 target
。请你从 nums
中找出并返回总和为 target
的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
示例 2:
输入:nums = [9], target = 3
输出:0
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 1000
nums
中的所有元素 互不相同1 <= target <= 1000
<2> 笔记
如果把上一题搞懂了,本题代码就可以直接写出了,与上题不同的是本题是一个排列问题(虽然名字叫排列总和 IV),但题目中的解释中出现了:
请注意,顺序不同的序列被视作不同的组合。
通过上面的推导其实就知道了,利用滚动数组先遍历背包得到的就是排列的情况,这里直接写出代码。
不要忘记初始化 dp[0] = 1
这是推导的基础
<3> 代码
class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1;
for (int j = 0; j <= target; j++) {
for (int i = 0; i < nums.length; i++) {
if (j >= nums[i]) dp[j] = dp[j] + dp[j - nums[i]];
}
}
return dp[target];
}
}