leetcode-master/problems/0518.零钱兑换II.md

265 lines
7.8 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p align="center">
<a href="https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ" target="_blank">
<img src="https://code-thinking-1253855093.file.myqcloud.com/pics/20210924105952.png" width="1000"/>
</a>
<p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
# 动态规划:给你一些零钱,你要怎么凑?
## 518. 零钱兑换 II
[力扣题目链接](https://leetcode-cn.com/problems/coin-change-2/)
难度:中等
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。 
示例 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 位符号整数
## 思路
这是一道典型的背包问题,一看到钱币数量不限,就知道这是一个完全背包。
对完全背包还不了解的同学,可以看这篇:[动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html)
但本题和纯完全背包不一样,**纯完全背包是能否凑成总金额,而本题是要求凑成总金额的个数!**
注意题目描述中是凑成总金额的硬币组合数,为什么强调是组合数呢?
例如示例一:
5 = 2 + 2 + 1
5 = 2 + 1 + 2
这是一种组合,都是 2 2 1。
如果问的是排列数,那么上面就是两种排列了。
**组合不强调元素之间的顺序,排列强调元素之间的顺序**。 其实这一点我们在讲解回溯算法专题的时候就讲过了哈。
那我为什么要介绍这些呢,因为这和下文讲解遍历顺序息息相关!
回归本题,动规五步曲来分析如下:
1. 确定dp数组以及下标的含义
dp[j]凑成总金额j的货币组合数为dp[j]
2. 确定递推公式
dp[j] 考虑coins[i]的组合总和) 就是所有的dp[j - coins[i]]不考虑coins[i])相加。
所以递推公式dp[j] += dp[j - coins[i]];
**这个递推公式大家应该不陌生了我在讲解01背包题目的时候在这篇[动态规划:目标和!](https://programmercarl.com/0494.目标和.html)中就讲解了求装满背包有几种方法一般公式都是dp[j] += dp[j - nums[i]];**
3. dp数组如何初始化
首先dp[0]一定要为1dp[0] = 1是 递归公式的基础。
从dp[i]的含义上来讲就是凑成总金额0的货币组合数为1。
下标非0的dp[j]初始化为0这样累计加dp[j - coins[i]]的时候才不会影响真正的dp[j]
4. 确定遍历顺序
本题中我们是外层for循环遍历物品钱币内层for遍历背包金钱总额还是外层for遍历背包金钱总额内层for循环遍历物品钱币
我在[动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html)中讲解了完全背包的两个for循环的先后顺序都是可以的。
**但本题就不行了!**
因为纯完全背包求得是能否凑成总和,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行!
而本题要求凑成总和的组合数,元素之间要求没有顺序。
所以纯完全背包是能凑成总和就行,不用管怎么凑的。
本题是求凑出来的方案个数,且每个方案个数是为组合数。
那么本题两个for循环的先后顺序可就有说法了。
我们先来看 外层for循环遍历物品钱币内层for遍历背包金钱总额的情况。
代码如下:
```CPP
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
dp[j] += dp[j - coins[i]];
}
}
```
假设coins[0] = 1coins[1] = 5。
那么就是先把1加入计算然后再把5加入计算得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况。
**所以这种遍历顺序中dp[j]里计算的是组合数!**
如果把两个for交换顺序代码如下
```
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]];
}
}
```
背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。
**此时dp[j]里算出来的就是排列数!**
可能这里很多同学还不是很理解,**建议动手把这两种方案的dp数组数值变化打印出来对比看一看实践出真知**
5. 举例推导dp数组
输入: amount = 5, coins = [1, 2, 5] dp状态图如下
![518.零钱兑换II](https://img-blog.csdnimg.cn/20210120181331461.jpg)
最后红色框dp[amount]为最终结果。
以上分析完毕C++代码如下:
```CPP
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount + 1, 0);
dp[0] = 1;
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
};
```
是不是发现代码如此精简,哈哈
## 总结
本题的递推公式,其实我们在[动态规划:目标和!](https://programmercarl.com/0494.目标和.html)中就已经讲过了,**而难点在于遍历顺序!**
在求装满背包有几种方案的时候,认清遍历顺序是非常关键的。
**如果求组合数就是外层for循环遍历物品内层for遍历背包**
**如果求排列数就是外层for遍历背包内层for循环遍历物品**
可能说到排列数录友们已经有点懵了后面Carl还会安排求排列数的题目到时候在对比一下大家就会发现神奇所在
## 其他语言版本
Java
```Java
class Solution {
public int change(int amount, int[] coins) {
//递推表达式
int[] dp = new int[amount + 1];
//初始化dp数组表示金额为0时只有一种情况也就是什么都不装
dp[0] = 1;
for (int i = 0; i < coins.length; i++) {
for (int j = coins[i]; j <= amount; j++) {
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
}
```
Python
```python3
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
dp = [0]*(amount + 1)
dp[0] = 1
# 遍历物品
for i in range(len(coins)):
# 遍历背包
for j in range(coins[i], amount + 1):
dp[j] += dp[j - coins[i]]
return dp[amount]
```
Go
```go
func change(amount int, coins []int) int {
// 定义dp数组
dp := make([]int, amount+1)
// 初始化,0大小的背包, 当然是不装任何东西了, 就是1种方法
dp[0] = 1
// 遍历顺序
// 遍历物品
for i := 0 ;i < len(coins);i++ {
// 遍历背包
for j:= coins[i] ; j <= amount ;j++ {
// 推导公式
dp[j] += dp[j-coins[i]]
}
}
return dp[amount]
}
```
Javascript
```javascript
const change = (amount, coins) => {
let dp = Array(amount + 1).fill(0);
dp[0] = 1;
for(let i =0; i < coins.length; i++) {
for(let j = coins[i]; j <= amount; j++) {
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
```
-----------------------
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码一.jpg width=500> </img></div>