leetcode-master/problems/0322.零钱兑换.md

478 lines
14 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://programmercarl.com/other/xunlianying.html" target="_blank">
<img src="../pics/训练营.png" width="1000"/>
</a>
<p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
# 322. 零钱兑换
[力扣题目链接](https://leetcode.cn/problems/coin-change/)
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1
* 输入coins = [1, 2, 5], amount = 11
* 输出3
* 解释11 = 5 + 5 + 1
示例 2
* 输入coins = [2], amount = 3
* 输出:-1
示例 3
* 输入coins = [1], amount = 0
* 输出0
示例 4
* 输入coins = [1], amount = 1
* 输出1
示例 5
* 输入coins = [1], amount = 2
* 输出2
提示:
* 1 <= coins.length <= 12
* 1 <= coins[i] <= 2^31 - 1
* 0 <= amount <= 10^4
## 算法公开课
**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html)[装满背包最少的物品件数是多少?| LeetCode322.零钱兑换](https://www.bilibili.com/video/BV14K411R7yv/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。
## 思路
在[动态规划518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html)中我们已经兑换一次零钱了,这次又要兑换,套路不一样!
题目中说每种硬币的数量是无限的,可以看出是典型的完全背包问题。
动规五部曲分析如下:
1. 确定dp数组以及下标的含义
**dp[j]凑足总额为j所需钱币的最少个数为dp[j]**
2. 确定递推公式
凑足总额为j - coins[i]的最少个数为dp[j - coins[i]]那么只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1就是dp[j]考虑coins[i]
所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的。
递推公式dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
3. dp数组如何初始化
首先凑足总金额为0所需钱币的个数一定是0那么dp[0] = 0;
其他下标对应的数值呢?
考虑到递推公式的特性dp[j]必须初始化为一个最大的数否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。
所以下标非0的元素都是应该是最大值。
代码如下:
```
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
```
4. 确定遍历顺序
本题求钱币最小个数,**那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数**。
所以本题并不强调集合是组合还是排列。
**如果求组合数就是外层for循环遍历物品内层for遍历背包**
**如果求排列数就是外层for遍历背包内层for循环遍历物品**
在动态规划专题我们讲过了求组合数是[动态规划518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html),求排列数是[动态规划377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和Ⅳ.html)。
**所以本题的两个for循环的关系是外层for循环遍历物品内层for遍历背包或者外层for遍历背包内层for循环遍历物品都是可以的**
那么我采用coins放在外循环target在内循环的方式。
本题钱币数量可以无限使用,那么是完全背包。所以遍历的内循环是正序
综上所述遍历顺序为coins物品放在外循环target背包在内循环。且内循环正序。
5. 举例推导dp数组
以输入coins = [1, 2, 5], amount = 5为例
![322.零钱兑换](https://code-thinking-1253855093.file.myqcloud.com/pics/20210201111833906.jpg)
dp[amount]为最终结果。
以上分析完毕C++ 代码如下:
```CPP
// 版本一
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包
if (dp[j - coins[i]] != INT_MAX) { // 如果dp[j - coins[i]]是初始值则跳过
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
}
}
}
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
```
* 时间复杂度: O(n * amount),其中 n 为 coins 的长度
* 空间复杂度: O(amount)
对于遍历方式遍历背包放在外循环,遍历物品放在内循环也是可以的,我就直接给出代码了
```CPP
// 版本二
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= amount; i++) { // 遍历背包
for (int j = 0; j < coins.size(); j++) { // 遍历物品
if (i - coins[j] >= 0 && dp[i - coins[j]] != INT_MAX ) {
dp[i] = min(dp[i - coins[j]] + 1, dp[i]);
}
}
}
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
```
* 同上
## 总结
细心的同学看网上的题解,**可能看一篇是遍历背包的for循环放外面看一篇又是遍历背包的for循环放里面看多了都看晕了**到底两个for循环应该是什么先后关系。
能把遍历顺序讲明白的文章几乎找不到!
这也是大多数同学学习动态规划的苦恼所在,有的时候递推公式很简单,难在遍历顺序上!
但最终又可以稀里糊涂的把题目过了,也不知道为什么这样可以过,反正就是过了。
那么这篇文章就把遍历顺序分析的清清楚楚。
[动态规划518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html)中求的是组合数,[动态规划377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和Ⅳ.html)中求的是排列数。
**而本题是要求最少硬币数量硬币是组合数还是排列数都无所谓所以两个for循环先后顺序怎样都可以**
这也是我为什么要先讲518.零钱兑换II 然后再讲本题即322.零钱兑换这是Carl的良苦用心那。
相信大家看完之后,对背包问题中的遍历顺序有更深的理解了。
## 其他语言版本
### Java
```Java
class Solution {
public int coinChange(int[] coins, int amount) {
int max = Integer.MAX_VALUE;
int[] dp = new int[amount + 1];
//初始化dp数组为最大值
for (int j = 0; j < dp.length; j++) {
dp[j] = max;
}
//当金额为0时需要的硬币数目为0
dp[0] = 0;
for (int i = 0; i < coins.length; i++) {
//正序遍历:完全背包每个硬币可以选择多次
for (int j = coins[i]; j <= amount; j++) {
//只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
if (dp[j - coins[i]] != max) {
//选择硬币数目最小的情况
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
}
}
}
return dp[amount] == max ? -1 : dp[amount];
}
}
```
### Python
先遍历物品 后遍历背包
```python
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1) # 创建动态规划数组,初始值为正无穷大
dp[0] = 0 # 初始化背包容量为0时的最小硬币数量为0
for coin in coins: # 遍历硬币列表,相当于遍历物品
for i in range(coin, amount + 1): # 遍历背包容量
if dp[i - coin] != float('inf'): # 如果dp[i - coin]不是初始值,则进行状态转移
dp[i] = min(dp[i - coin] + 1, dp[i]) # 更新最小硬币数量
if dp[amount] == float('inf'): # 如果最终背包容量的最小硬币数量仍为正无穷大,表示无解
return -1
return dp[amount] # 返回背包容量为amount时的最小硬币数量
```
先遍历背包 后遍历物品
```python
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1) # 创建动态规划数组,初始值为正无穷大
dp[0] = 0 # 初始化背包容量为0时的最小硬币数量为0
for i in range(1, amount + 1): # 遍历背包容量
for j in range(len(coins)): # 遍历硬币列表,相当于遍历物品
if i - coins[j] >= 0 and dp[i - coins[j]] != float('inf'): # 如果dp[i - coins[j]]不是初始值,则进行状态转移
dp[i] = min(dp[i - coins[j]] + 1, dp[i]) # 更新最小硬币数量
if dp[amount] == float('inf'): # 如果最终背包容量的最小硬币数量仍为正无穷大,表示无解
return -1
return dp[amount] # 返回背包容量为amount时的最小硬币数量
```
先遍历物品 后遍历背包(优化版)
```python
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin in coins:
for i in range(coin, amount + 1): # 进行优化,从能装得下的背包开始计算,则不需要进行比较
# 更新凑成金额 i 所需的最少硬币数量
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
```
先遍历背包 后遍历物品(优化版)
```python
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1): # 遍历背包容量
for coin in coins: # 遍历物品
if i - coin >= 0:
# 更新凑成金额 i 所需的最少硬币数量
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
```
### Go
```go
// 版本一, 先遍历物品,再遍历背包
func coinChange1(coins []int, amount int) int {
dp := make([]int, amount+1)
// 初始化dp[0]
dp[0] = 0
// 初始化为math.MaxInt32
for j := 1; j <= amount; j++ {
dp[j] = math.MaxInt32
}
// 遍历物品
for i := 0; i < len(coins); i++ {
// 遍历背包
for j := coins[i]; j <= amount; j++ {
if dp[j-coins[i]] != math.MaxInt32 {
// 推导公式
dp[j] = min(dp[j], dp[j-coins[i]]+1)
//fmt.Println(dp,j,i)
}
}
}
// 没找到能装满背包的, 就返回-1
if dp[amount] == math.MaxInt32 {
return -1
}
return dp[amount]
}
// 版本二,先遍历背包,再遍历物品
func coinChange2(coins []int, amount int) int {
dp := make([]int, amount+1)
// 初始化dp[0]
dp[0] = 0
// 遍历背包,从1开始
for j := 1; j <= amount; j++ {
// 初始化为math.MaxInt32
dp[j] = math.MaxInt32
// 遍历物品
for i := 0; i < len(coins); i++ {
if j >= coins[i] && dp[j-coins[i]] != math.MaxInt32 {
// 推导公式
dp[j] = min(dp[j], dp[j-coins[i]]+1)
//fmt.Println(dp)
}
}
}
// 没找到能装满背包的, 就返回-1
if dp[amount] == math.MaxInt32 {
return -1
}
return dp[amount]
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
```
### Rust:
```rust
// 遍历物品
impl Solution {
pub fn coin_change(coins: Vec<i32>, amount: i32) -> i32 {
let amount = amount as usize;
let mut dp = vec![i32::MAX; amount + 1];
dp[0] = 0;
for coin in coins {
for i in coin as usize..=amount {
if dp[i - coin as usize] != i32::MAX {
dp[i] = dp[i].min(dp[i - coin as usize] + 1);
}
}
}
if dp[amount] == i32::MAX {
return -1;
}
dp[amount]
}
}
```
```rust
// 遍历背包
impl Solution {
pub fn coin_change(coins: Vec<i32>, amount: i32) -> i32 {
let amount = amount as usize;
let mut dp = vec![i32::MAX; amount + 1];
dp[0] = 0;
for i in 1..=amount {
for &coin in &coins {
if i >= coin as usize && dp[i - coin as usize] != i32::MAX {
dp[i] = dp[i].min(dp[i - coin as usize] + 1)
}
}
}
if dp[amount] == i32::MAX {
return -1;
}
dp[amount]
}
}
```
### Javascript
```javascript
// 遍历物品
const coinChange = (coins, amount) => {
if(!amount) {
return 0;
}
let dp = Array(amount + 1).fill(Infinity);
dp[0] = 0;
for(let i = 0; i < coins.length; i++) {
for(let j = coins[i]; j <= amount; j++) {
dp[j] = Math.min(dp[j - coins[i]] + 1, dp[j]);
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}
```
```javascript
// 遍历背包
var coinChange = function(coins, amount) {
const dp = Array(amount + 1).fill(Infinity)
dp[0] = 0
for (let i = 1; i <= amount; i++) {
for (let j = 0; j < coins.length; j++) {
if (i >= coins[j] && dp[i - coins[j]] !== Infinity) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1)
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount]
}
```
### TypeScript
```typescript
// 遍历物品
function coinChange(coins: number[], amount: number): number {
const dp: number[] = new Array(amount + 1).fill(Infinity);
dp[0] = 0;
for (let i = 0; i < coins.length; i++) {
for (let j = coins[i]; j <= amount; j++) {
if (dp[j - coins[i]] === Infinity) continue;
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
};
```
```typescript
// 遍历背包
function coinChange(coins: number[], amount: number): number {
const dp: number[] = Array(amount + 1).fill(Infinity)
dp[0] = 0
for (let i = 1; i <= amount; i++) {
for (let j = 0; j < coins.length; j++) {
if (i >= coins[j] && dp[i - coins[j]] !== Infinity) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1)
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount]
}
```
<p align="center">
<a href="https://programmercarl.com/other/kstar.html" target="_blank">
<img src="../pics/网站星球宣传海报.jpg" width="1000"/>
</a>