题目:518. 零钱兑换 II
题目描述:
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/coin-change-ii
示例:
示例 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
思路:
题目划分[动态规划-背包问题-完全背包]:
- 这是一道典型的背包问题,看到钱币的数量不限,就知道这是一个完全背包问题
- 与纯完全背包不一样的是,纯完全背包是能否凑成总⾦额,⽽本题是要求凑成总⾦额的个数
题目坑点:
- 题目要求金币的组合数
- 组合数和排列数:组合不强调元素之间的顺序,排列强调元素之间的顺序
动态规划五步曲:
- 确定dp数组以及下标的含义
(1)dp[i]:凑成总⾦额 i 的货币组合数为dp[i]
(2)二维dp[i] [j] 表示使用0~i金币凑成总⾦额j的货币组合数为dp[i] [j],但是可以通过滚动数组的思想变成上述一维数组dp[i]
- 确定递推公式
递推公式:dp[j] += dp[j - coins[i]];
含义:要求满足金额 j ,则遍历到 金币 coins[i] 时,当前组合数dp[j]就要加上满足金额j - conins[i] 时的组合数dp[j - conins[i]]。这也是背包问题里求装满背包有几种方法的一般公式。
- dp数组如何初始化
dp[0] = 1
含义:金额为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]];
}
}
/*
假设:coins[0] = 1,coins[1] = 5。
那么在外层循环会先把1加⼊计算,然后再把5加⼊计算,得到的⽅法数量只有{1, 5}这种情况。⽽不会出现{5, 1}的情况。
我们得到的就是**组合数**
*/
假如外层循环遍历金额(背包),内层循环遍历金币(物品),情况如下
(相关题目377. 组合总和 Ⅳ)
for(int j = 1; j <= amount; j++){
for(int i = 0; i < coins.length; i++){
if(j - coins[i] >= 0)
dp[j] += dp[j - coins[i]];
}
}
/*
这种情况下,经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。
例如:金额为6,遇到金币1,会加上dp[5]的情况,遇到金币5,会加上dp[1]的情况
导致得到的是**排列数**
*/
5.举例推导dp数组
根据上述分析,推导几个dp数组中的值,对比理论和实际的情况是否吻合
代码如下:
class Solution {
public int change(int amount, int[] coins) {
int n = coins.length;
// 1.凑成总⾦额 i 的货币组合数为dp[i]
int[] dp = new int[amount + 1];
// 2.确定递推公式
// 3.初始化
dp[0] = 1;
// 4.确定遍历顺序
for(int i = 0; i < n; i++){ // 金币(物品)
for(int j = coins[i]; j <= amount; j++){// 金额(背包重量)
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
}