【LeetCode动态规划#08】完全背包问题实战与分析(零钱兑换II)

这篇具有很好参考价值的文章主要介绍了【LeetCode动态规划#08】完全背包问题实战与分析(零钱兑换II)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

零钱兑换II

力扣题目链接(opens new window)

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 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

注意,你可以假设:

  • 0 <= amount (总金额) <= 5000
  • 1 <= coin (硬币面额) <= 5000
  • 硬币种类不超过 500 种
  • 结果符合 32 位符号整数

思路

经典背包问题

关键字眼:钱币数量不限-->完全背包

注意题目要求,本题是要求:可以凑成总金额的硬币组合数,是组合个数;而单纯的完全背包(或者说背包问题)要求的是能够凑成背包的最大价值

因此,本题属于背包问题在求排列组合时的应用(那就与目标和那题一样),具体到本题是求组合

组合与排列的区别

例如示例一:

5 = 2 + 2 + 1

5 = 2 + 1 + 2

这是一种组合,都是 2 2 1。

如果问的是排列数,那么上面就是两种排列了。

组合不强调元素之间的顺序,排列强调元素之间的顺序

详见:确定遍历顺序(目标和)

五步走

1、确定dp数组含义

dp[j]:能凑成总金额为j的货币组合个数为dp[j]

对应到题目中,j就是amount,即背包容量

而coins数组中的元素就是物品

2、确定递推公式

和目标和中的递推公式完全一样,这里再简要推导一下

推导过程一切遵循dp数组含义

假设amount = 3,即背包容量是3

那么在还没往里面放东西的时候,此时还有3的容量,那么就还能够有dp[3]种能凑出总金额为3的货币组合

若已经确定要放入1个货币,那此时容量就要减1,还剩2个容量,那么就还能有dp[2]种能凑出总金额为3的货币组合

若已经确定要放入2个货币,那此时容量就要减2,还剩1个容量,那么就还能有dp[1]种能凑出总金额为3的货币组合

若已经确定要放入3个货币,那此时容量就要减3,还剩0个容量,那么就还能有dp[0]种能凑出总金额为3的货币组合

从下往上看,以上描述就是将货币数组coins中的物品一个一个放入背包的过程

最后能够得到dp[3],因此dp[3]是累加而来的

所以递推公式是:dp[j] += dp[j - coins[i]];

3、初始化dp数组

还是和目标和那题一样,当j为0时,需要初始化为1,也就是说,容量为0也有一种组合

dp[0]=1;

其余情况初始化为0,使后面递推得到的值能够累加起来

4、确定遍历顺序

完全背包问题是正序遍历的;(因为物品不可以重复使用)

01背包问题是倒序遍历的;(因为物品可以重复使用)

这里还需要讨论一下两层for循环中遍历物品和背包容量先后顺序的问题

情况1

情况1:

for(int i = 0; i < coins.size(); ++i){//先遍历物品
 for(int j = coins[i]; j <= amount; ++i){//再遍历背包容量
     dp[j] += dp[j - coins[i]];
 }
}

来模拟一下物品放入,并遍历背包容量的过程

假设amount = 5,coins = [1, 2, 5]

从1开始遍历,遍历到2满足条件,此时组合为{1,2},这种顺序下不会遍历得到{2,1},因为2永远在1后面被用来尝试放入背包

所以先物品后容量的顺序得到的是物品放入背包的组合数

这么说可能有点抽象,我其实一直不清楚:为什么第二层for循环中循环变量的初始值是coins[i],以及为什么每轮循环后其都要递增

所以尝试模拟推导一下dp数组:

    0  1  2  3  4  5(背包容量)
    1  0  0  0  0  0 (没有硬币的时候)
=======================
    0  1  2  3  4  5(背包容量)
1   1  1  1  1  1  1
=======================
    0  1  2  3  4  5(背包容量)
1   1  1  1  1  1  1
2         2  2  3  3
有了面值为2的硬币后,哎,我就是不用,所以方案数还是dp[j]种;
但是我如果用了,那我看看在放入这枚硬币前,也就是背包容量为[j-coins[i]]的时候有几种方案;
两种情况加起来,所以就是  dp[j] = dp[j]+dp[j-coins[i]];
========================
    0  1  2  3  4  5(背包容量)
1   1  1  1  1  1  1
2         2  2  3  3
5                  4

上述解释摘自https://leetcode.cn/problems/coin-change-ii/solution/gong-shui-san-xie-xiang-jie-wan-quan-bei-6hxv/979973

这里解释了为什么第二层for循环每次都要从coins[i]开始遍历

拿coins[1],即2来举例,硬币面值是2,那背包容量为0和1时自然不可能放下,所以至少要等背包容量大于等于2时才能考虑使用面值为2的硬币

而当第二层for循环的循环变量以coins[1]为初始值时,就意味着要开始找面值为2的硬币放入背包的组合有几种了

还是拿上面的来看,当有了面值为2的硬币后,如果背包容量大于等于2,那么可以用面值为2的硬币配合之前获得的面值为1的硬币来组合出当前背包容量(面值)。

这件事情怎么说会比较好呢?

也就是说,如果我们现在有了面值为2的硬币,那么在凑容量大于2的背包时就可以用这个面值的硬币配合之前面值的硬币来凑出背包容量

但是,实际上不用面值为2的硬币我们也能用之前的硬币凑出对应的背包容量

只是说用了新面值的硬币之后,凑出背包容量的组合数量就增加了

当只有面值为1的硬币时,要凑背包容量2,只有一种组合:1、1;

当有面值为1、2的硬币时,除了1、1以外,还可以有2,组合数量增加为2;其他背包容量以此类推

联系dp数组的含义dp[j]表示能凑成总金额为j的货币组合个数为dp[j]

那dp[2]就是能凑出总金额为2的货币组合个,

我们用面值为1的硬币也能凑出来总金额2,且组合有1+1+1+1+1=5种,即dp[1];

在此基础上加入面值为2的硬币,也能凑出总金额2,组合数量在上面的基础上增加1+1+2+2+3+3=10种,即dp[2]

所以,dp[2] = dp[2] + dp[1];

这就和递推公式dp[j] += dp[j - coins[i]];对应上了

回到最开始的疑问,为什么第二层for循环每次都要从coins[i]开始遍历

现在能够理解了,其实就是为了分开计算组合的情况,从coins[i]开始之后计算得到的dp数组的值(也就是组合种类),是包含了使用当前新加入的coins[i]面值的硬币之后的组合种类

而代码就是在模拟这个操作,至于j为什么每次都要递增。

因为j其实只是在遍历背包容量时的一个指针,j的递增跟coins[i]没有关系

情况2

情况2:

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

还是模拟一下,

遍历背包容量0,都放不下

遍历背包容量1,可以放一个1,此时能够凑齐容量的集合有:{1}

遍历背包容量2,此时coins中的1和2都可以放入,此时能够凑齐容量的集合有:{1,1}、{2}

遍历背包容量3,此时coins中的1和2都可以放入,此时能够凑齐容量的集合有:{1,1,1}、{1,2}、{2,1}

...之后的集合以此类推

为什么这里会出现{1,2}、{2,1},因为先遍历背包容量时,相当于拿背包容量去尝试套硬币的面值

此时我们是默认有所有面值的硬币的,因此一旦有能够用背包容量套下的硬币集合,那么该集合的所有方式都会被遍历出来

因此,先容量后物品的顺序得到的是物品放入背包的排列数

代码

代码其实就是按照模板写就好了,其中比较"细思恐极"的部分再上面已经详细解释了

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        //定义并初始化dp数组
        vector<int> dp(amount + 1, 0);
        dp[0] = 1;
        //遍历dp数组
        for(int i = 0; i < coins.size(); ++i){//遍历物品
            //第二层for中循环变量的初始值j是第一层for循环遍历到的物品的重量(容量)
            //用于定位到大于等于当前新加入硬币面值的背包容量位置
            for(int j = coins[i]; j <= amount; ++j){//遍历背包容量
                dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
};

组合总和IV

力扣题目链接(opens new window)

难度:中等

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

  • nums = [1, 2, 3]
  • target = 4

所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)

请注意,顺序不同的序列被视作不同的组合。

因此输出为 7。

思路

题目给的示例中有一个注意事项,"顺序不同的序列被视作不同的组合"

也就是说,本题要求的是排列而不是组合(求组合的完全背包可以看 零钱兑换II)

根据之前题目的分析可以知道,dp数组的遍历顺序会影响所求的结果

先物品后容量,得到的是组合;

先容量后物品,得到的是排列;

代码

本题五步走分析和之前的完全背包是一样的,包括dp数组的含义也都是一样的,直接来看代码就行

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        //定义dp数组
        vector<int> dp(target + 1, 0);
        //初始化
        dp[0] = 1;
        //遍历dp数组
        for(int j = 0; j <= target; ++j){//先遍历物品
            for(int i = 0; i < nums.size(); ++i){//后遍历背包容量
                if(j - nums[i] >= 0 && dp[j] < INT_MAX - dp[j - nums[i]]) dp[j] += dp[j - nums[i]];
            }
        }
        return dp[target];
    }
};

一些解释:

1、j - nums[i] >= 0

拓展:本题与爬楼梯的联系

回忆一下 爬楼梯 ,本题的要求是我们一步可以爬1个或2个台阶,问爬到楼顶(也就是n个台阶)一共有几种方法

如果题目条件变一下,一步可以爬3个台阶、4个、m个呢?

那其实“一步可以爬几个台阶”,“几个台阶”就相当于本题nums数组中的元素,即物品

如果是一步可以爬3个台阶,那就是数组从1到3

假设爬到楼顶有n阶楼梯,那这个n就是target,即背包容量

而我们要求的就是所有爬到楼顶方法的排列(假设n=3,先爬1步再爬2步和先爬2步再爬1步显然是两种不同的上楼方法)文章来源地址https://www.toymoban.com/news/detail-418228.html

到了这里,关于【LeetCode动态规划#08】完全背包问题实战与分析(零钱兑换II)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 动态规划-背包问题-完全背包

    对比01背包,完全背包中的每件物品有无数件。 也就是说,每件物品可以拿0,1,…,k,…件。 dp[i][j]表示前i种物品,体积为j时的最大价值 对于第i件物品: 不拿:dp[i][j]⇐dp[i-1][j] 拿一件:dp[i][j]⇐dp[i-1][j-w[i]]+v[i] 拿两件:dp[i][j]⇐dp[i-1][j-2w[i]]+2v[i] … 拿k件:dp[i]][j]⇐dp[i

    2024年04月08日
    浏览(49)
  • 完全背包&多重背包问题(动态规划)

    完全背包问题: 每个物品使用次数没有限制,与0-1背包的不同之处在于 遍历背包的顺序 是正序。 多重背包问题: 与完全背包的区别在于,每一种物品是有个数限制的,不能无限选择。这篇博客讲解的非常详细,可以参考学习: 多重背包问题---超详细讲解+优化(不懂你揍我

    2024年04月10日
    浏览(48)
  • 动态规划之背包问题——完全背包

    算法相关数据结构总结: 序号 数据结构 文章 1 动态规划 动态规划之背包问题——01背包 动态规划之背包问题——完全背包 动态规划之打家劫舍系列问题 动态规划之股票买卖系列问题 动态规划之子序列问题 算法(Java)——动态规划 2 数组 算法分析之数组问题 3 链表 算法

    2024年02月03日
    浏览(63)
  • 【LeetCode动态规划#07】01背包问题一维写法(状态压缩)实战,其二(目标和、零一和)

    力扣题目链接(opens new window) 难度:中等 给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。 返回可以使最终数组和为目标数 S 的所有添加符号的方法数。 示例: 输入

    2023年04月18日
    浏览(63)
  • 【动态规划之完全背包问题】完全背包问题的通用解法与优化

    ⭐️ 前面的话 ⭐️ 本篇文章将介绍动态规划中的背包问题——完全背包问题,前面我们已经介绍了0-1背包问题,其实完全背包问题就只改了0-1背包问题的一个条件,即物品可选择次数由一次改为无数次,仅此而已,下面我们就来开始介绍完全背包问题。 📒博客主页:未见

    2023年04月22日
    浏览(49)
  • 动态规划:完全背包问题

    ACwing #3. 完全背包问题 完全背包问题和01背包问题很相似。 01背包问题每个物品只能选一个,而完全背包问题每个物品可以选无限次。 DP问题的关键是找到状态转移方程: ①定义f[i][j]表示从前 i 个物品中选择,体积为 j 的时候的最大价值。 ②那么转移方程f[i][j] = max(f[i - 1][j

    2023年04月19日
    浏览(45)
  • 动态规划——完全背包问题

    由于本人实力尚浅,接触算法没多久,写这篇blog仅仅是想要提升自己对算法的理解,如果各位读者发现什么错误,恳请指正,希望和大家一起进步。(●’◡’●) 了解完全背包问题前可以先去看看01背包问题(良心正解),先了解这个基础问题会更有利于你了解下面的完全背

    2024年02月04日
    浏览(48)
  • 算法系列--动态规划--背包问题(3)--完全背包介绍

    💕\\\"Su7\\\"💕 作者:Lvzi 文章主要内容:算法系列–动态规划–背包问题(3)–完全背包介绍 大家好,今天为大家带来的是 算法系列--动态规划--背包问题(3)--完全背包介绍 链接: 完全背包 可以发现完全背包问题和01背包问题还是特比相似的 分析: 完全背包问题 是 01背包问题 的推广

    2024年04月25日
    浏览(44)
  • 动态规划完全背包问题-java

    完全背包问题跟01背包问题思路大致一样,只不过对于物品的拿取次数不在限制,我们只需要考虑这点即可。 文章目录 前言 一、什么是完全背包问题? 二、问题模拟 1.样例数据 2.算法思路 三、代码如下 1.代码如下(示例): 2.读入数 3.代码运行结果 总结 完全背包问题跟

    2024年04月26日
    浏览(44)
  • 动态规划-----背包类问题(0-1背包与完全背包)详解

    目录 什么是背包问题? 动态规划问题的一般解决办法: 0-1背包问题: 0 - 1背包类问题  分割等和子集:  完全背包问题:  完全背包类问题 零钱兑换II: 背包问题(Knapsack problem)是一种组合优化的NP完全问题。 问题可以描述为:给定一组物品,每种物品都有自己的重量和价格

    2024年04月17日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包