代码随想录刷题笔记 DAY 43 | 完全背包基础 | 零钱兑换 II No.518 | 组合总和 IV No.377

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

物品编号weightvalue
0115
1320
2430

  • 对于 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 11 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 11 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];
    }
}
  • 20
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

*Soo_Young*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值