一、题目内容 中等
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例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
二、解题思路
这是一道典型的背包问题,一看到钱币数量不限,就知道这是一个完全背包。
但本题和纯完全背包不一样,纯完全背包是能否凑成总金额,而本题是要求凑成总金额的个数!
注意题目描述中是凑成总金额的硬币组合数,为什么强调是组合数呢?
例如示例一:
5 = 2 + 2 + 1
5 = 2 + 1 + 2
这是一种组合,都是 2 2 1。
如果问的是排列数,那么上面就是两种排列了。
组合不强调元素之间的顺序,排列强调元素之间的顺序。 这一点我们在讲解回溯算法专题的时候就讲过了
那我为什么要介绍这些呢,因为这和下文讲解遍历顺序息息相关!
回归本题,动规五步曲来分析如下:
- 确定dp数组以及下标的含义
dp[j]
:凑成总金额 j 的货币组合数
- 确定递推公式
dp[j]
(考虑coins[i]
的组合总和) 就是所有的dp[j - coins[i]]
相加。
所以递推公式:dp[j] += dp[j - coins[i]]
:::info
这个递推公式大家应该不陌生了,我在讲解01背包题目的时候在 8. 目标和(494) 中就讲解了
求装满背包有几种方法,一般公式都是:**dp[j] += dp[j - nums[i]]**
:::
- dp 数组如何初始化
首先dp[0]
一定要为 1,dp[0] = 1
是 递归公式的基础
为啥是递归的基础? 你想想
dp[1]
的值怎么来的就知道了
从dp[i]
的含义上来讲就是,凑成总金额 0 的货币组合数为 1。
- 确定遍历顺序
本题中我们是外层 for 循环遍历物品(钱币),内层 for 遍历背包(金钱总额)
还是外层 for 遍历背包(金钱总额),内层 for 循环遍历物品(钱币)呢?
背包理论之完全背包中讲解了完全背包的两个 for 循环的先后顺序都是可以的。但本题就不行了!
因为纯完全背包求得是能否凑成总和,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行!
而本题要求凑成总和的组合数,元素之间要求没有顺序。
那么本题,两个 for 循环的先后顺序可就有说法了。
我们先来看 外层 for 循环遍历物品(钱币),内层 for 遍历背包(金钱总额)的情况。
// 先遍历物品
for (let i = 0; i < coins.length; i++) {
// 后遍历背包
for (let j = coins[i]; j <= amount; j++) {
dp[j] += (dp[j - coins[i]] || 0)
}
}
假设:coins[0] = 1,coins[1] = 5。
那么就是先把 1 加入计算,然后再把 5 加入计算,得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况。
所以这种遍历顺序中dp[j]里计算的是组合数!
如果把两个 for 交换顺序,代码如下:
// 先遍历背包
for (let j = 0; j <= amount; j++) {
// 后遍历物品
for (let i = 0; i < coins.length; i++) {
if(j >= coins[i]) dp[j] += (dp[j - coins[i]] || 0)
}
}
背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。
此时**dp[j]**
里算出来的就是排列数!
- 举例推导 dp 数组
输入: amount = 5, coins = [1, 2, 5] ,dp状态图如下:
最后红色框 dp[amount]
为最终结果。
三、具体代码
/**
* @param {number} amount
* @param {number[]} coins
* @return {number}
*/
var change = function (amount, coins) {
const dp = new 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]] || 0)
}
}
return dp[amount]
};