零钱兑换 II(题号518)
题目
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 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
位符号整数
链接
解释
尽力了,这题真没想到解决方案。
一开始想到的递归,和上一题的递归类似,稍微修改一下即可,但是最后的结果必然是超时。
后来开始往递归上着补,最后也是没补出来,实在GET不到硬币组合数的关系,找不到关系自然找不到递归方程了,理所应当的GG。
可尴尬的问题来了,最后即使看了答案也是一知半解,花了很久的时候才稍稍理解,又点迷糊。
整体的逻辑是这样的(嫖的lee Chen老哥的解释,具体可以点击进去看看):
-
假设输入: amount = 5, coins = [1, 2, 5],如果用长度为amount + 1的dp数组递推,需要满足如下特性:
- 如果用长度为amount + 1的dp数组递推。
- 每个索引代表了递推到的金额。
- 每个元素是硬币组合数。
- 0位置是初始状态,设置为1,表示都从1种组合数开始递推。
-
我们可以将其按照硬币种类拆分递推结果:
- 硬币1:[1,1,1,1,1,1]
- 硬币2:[1,0,1,0,1,0]
- 硬币5:[1,0,0,0,0,1]
- 硬币1、2:[1,1,2,2,3,3]
- 硬币1、2、5:[1,1,2,2,3,4]
-
递推方法如下:
- 依次计算每个硬币面额
coin
的递推结果。 - 当前金额i,是从
i - coin
添加了coin
而来。 - 当前新的硬币组合数
dp[i]
,等于上一种硬币递推到i的组合数dp[i]
,加上i - coin
的组合数dp[i - coin]
而来。 - 状态转移方程为:
dp[i] = dp[i] + dp[i - coin]
。
- 依次计算每个硬币面额
老哥说的很明白了,笔者再多说就是废话了,👇看代码:
自己的答案(递归)
var change = function(amount, coins) {
coins.sort((a, b) => b -a)
res = 0
var d = (amount, index, count) => {
if (amount === 0) return res++
if (index === coins.length) return
for (var n = amount / coins[index] | 0; n >= 0; n--)
d(amount - n * coins[index], index + 1, count + n)
}
d(amount, 0, 0)
return res
};
经典递归,和上一题类似,跑到第17个用例时超时GG。
更好的方法(动态规划:二维数组)
var change = function(amount, coins) {
if (amount === 0) return 1;
const dp = [Array(amount + 1).fill(1)];
for (let i = 1; i < amount + 1; i++) {
dp[i] = Array(coins.length + 1).fill(0);
for (let j = 1; j < coins.length + 1; j++) {
if (i - coins[j - 1] >= 0) {
dp[i][j] = dp[i][j - 1] + dp[i - coins[j - 1]][j];
} else {
dp[i][j] = dp[i][j - 1];
}
}
}
return dp[dp.length - 1][coins.length];
}
经典二维数组DP,dp[i]
是当前金额,dp[j]
是当前面额硬币的组合数。
当然了,对于经典DP来说,降维也是可以的👇:
更好的方法(动态规划:降维)
var change = function(amount, coins) {
if (amount === 0) return 1;
var dp = new Array(amount + 1).fill(0)
dp[0] = 1
for (const coin of coins) {
for (let i = 1; i < amount + 1; i++) {
if (coin <= i) {
dp[i] = dp[i] + dp[i - coin]
}
}
}
return dp[amount]
};
可能会有点不好理解,这里把coin
的循环放到了外层,而把amount
循环放到了里面。
这可以理解的获取数据的顺序问题,如果coin
循环在外面,那么里面的循环会一层层的拿到所有coin
组合的数据;如果coin
循环在里面,那么只会拿到当前数字的硬币组合,后续数字无法拿到。
小结
这块的动态规划有人说是简单题,笔者直接自闭了,刷题有时候就是这样,有时候想不到那个点就一直想不出答案,想到那个点答案一下就出来了,还得继续练习啊!
本来以为找零钱是中国人都会的技能,看完这两题感觉自己是真的不会找零钱了。。。
PS:想查看往期文章和题目可以点击下面的链接:
这里是按照日期分类的👇
经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇
有兴趣的也可以看看我的个人主页👇