零钱兑换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的硬币我们也能用之前的硬币凑出对应的背包容量

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

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

【LeetCode动态规划#08】完全背包问题实战与分析(零钱兑换II)的更多相关文章

  1. Leetcode 518.零钱兑换II

    零钱兑换II 给定不同面额的硬币和一个总金额.写出函数来计算可以凑成总金额的硬币组合数.假设每一种面额的硬币有无限个. 注意: 你可以假设 0 <= amount (总金额) <= 500 ...

  2. Java实现 LeetCode 518 零钱兑换 II

    518. 零钱兑换 II 给定不同面额的硬币和一个总金额.写出函数来计算可以凑成总金额的硬币组合数.假设每一种面额的硬币有无限个. 示例 1: 输入: amount = 5, coins = [1, ...

  3. leetcode动态规划题目总结

    Hello everyone, I am a Chinese noob programmer. I have practiced questions on leetcode.com for 2 yea ...

  4. [LeetCode] 动态规划入门题目

    最近接触了动态规划这个厉害的方法,还在慢慢地试着去了解这种思想,因此就在LeetCode上面找了几道比较简单的题目练了练手. 首先,动态规划是什么呢?很多人认为把它称作一种"算法" ...

  5. 快速上手leetcode动态规划题

    快速上手leetcode动态规划题 我现在是初学的状态,在此来记录我的刷题过程,便于以后复习巩固. 我leetcode从动态规划开始刷,语言用的java. 一.了解动态规划 我上网查了一下动态规划,了 ...

  6. 【动态规划】简单背包问题II

    问题 B: [动态规划]简单背包问题II 时间限制: 1 Sec  内存限制: 64 MB提交: 21  解决: 14[提交][状态][讨论版] 题目描述 张琪曼:“为什么背包一定要完全装满呢?尽可能 ...

  7. 揪出“凶手”——实战WinDbg分析电脑蓝屏原因

    http://www.appinn.com/blue-screen-search-code/ 蓝屏代码查询器 – 找出蓝屏的元凶 11 文章标签: windows / 系统 / 蓝屏. 蓝屏代码查询器 ...

  8. Salesforce学习之路-developer篇(五)一文读懂Aura原理及实战案例分析

    1. 什么是Lightning Component框架? Lightning Component框架是一个UI框架,用于为移动和台式设备开发Web应用程序.这是一个单页面Web应用框架,用于为Ligh ...

  9. LeetCode:322. 零钱兑换

    链接:https://leetcode-cn.com/problems/coin-change/ 标签:动态规划.完全背包问题.广度优先搜索 题目 给定不同面额的硬币 coins 和一个总金额 amo ...

  10. LeetCode:零钱兑换【322】【DP】

    LeetCode:零钱兑换[322][DP] 题目描述 给定不同面额的硬币 coins 和一个总金额 amount.编写一个函数来计算可以凑成总金额所需的最少的硬币个数.如果没有任何一种硬币组合能组成 ...

随机推荐

  1. CF 1020B Badge

    之前写过DFS的题,但是或多或少有参考别人的思路,最近开始专攻DFS,下面这道题就是DFS练习第一题CF 1020B,之后训练完后抽时间写一篇DFS专项.   B. Badge time limit ...

  2. Q:oracle 日期筛选

    一.oracle where条件日期筛选 两种方法:tochar和todate todate:将字符串按照指定的格式输出,得到的是日期类型. to_date('2019-12-01','yyyy-MM ...

  3. 三、Applied visual design(应用视觉设计)

    一.文本属性整理 <style> h2 { /* 文字对齐属性 text-align */ /* center:居中,left,right:居左居右,justify:文字拉伸铺满一行 */ ...

  4. 快速排序(Java分治法)

    快速排序(Java分治法) 文章目录 快速排序(Java分治法) 0. 分治策略 1.思路步骤 2.代码 3.复杂度分析 3.1 最好情况 3.2 最坏情况 3.3 平均情况 3.4 性能影响因素 4 ...

  5. Python使用Eel和HTML开发桌面应用GUI直接用web前端的VUE+VANT来做

    python的gui太难用了,唯一能配置独立前端的程序只有web.所以用web做前端,到python,完美! 环境准备    Python 3.9    Chrome浏览器(由于Eel是直接调用的Ch ...

  6. 通过cpolar内网穿透 https://blog.csdn.net/CpolarLisa/article/details/128148698

    远程办公:通过cpolar内网穿透,远程桌面控制家里公司内网电脑_Cpolar Lisa的博客-CSDN博客 https://blog.csdn.net/CpolarLisa/article/deta ...

  7. mysql5.7主从搭建过程

    1.下载程序包 mysql-5.7.33-linux-glibc2.12-x86_64.tar.gz ,解压至/opt/app/mysql2.配置/etc/my.cnf[mysql]socket=/o ...

  8. supervisor不一样的日志轮转

    出于项目需求,需要读取某个进程的最新日志,而这个进程刚好是supervisor管控. 很自然地我就想到了,根据日志的编辑时间排序,获取最新的日志文件. 然而,发现了奇怪的一幕: 发现什么没有? web ...

  9. FPGA实现国密算法SM4

    本文基于FPGA实现高速SM4加密与解密,提供开源Verilog RTL设计和可综合工程:https://github.com/cassuto/SM4-FPGA. 本文仅讨论实现细节,不涉及算法原理. ...

  10. 【c语言】整数拆分

    将一个正整数n拆分成若干个正整数的和(至少两个数,n<=100). 输入格式: 一个正整数n 输出格式: 若干行,每行一个等式(数与数之间要求非降序排列).最后一行给出解的总个数 输入样例: 在 ...