前端刷题路-Day27:零钱兑换 II(题号518)

459 阅读2分钟

零钱兑换 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 位符号整数

链接

leetcode-cn.com/problems/co…

解释

尽力了,这题真没想到解决方案。

一开始想到的递归,和上一题的递归类似,稍微修改一下即可,但是最后的结果必然是超时。

后来开始往递归上着补,最后也是没补出来,实在GET不到硬币组合数的关系,找不到关系自然找不到递归方程了,理所应当的GG。

可尴尬的问题来了,最后即使看了答案也是一知半解,花了很久的时候才稍稍理解,又点迷糊。

整体的逻辑是这样的(嫖的lee Chen老哥的解释,具体可以点击进去看看):

  1. 假设输入: amount = 5, coins = [1, 2, 5],如果用长度为amount + 1的dp数组递推,需要满足如下特性:

    • 如果用长度为amount + 1的dp数组递推。
    • 每个索引代表了递推到的金额。
    • 每个元素是硬币组合数。
    • 0位置是初始状态,设置为1,表示都从1种组合数开始递推。
  2. 我们可以将其按照硬币种类拆分递推结果:

    • 硬币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]
  3. 递推方法如下:

    • 依次计算每个硬币面额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:想查看往期文章和题目可以点击下面的链接:

这里是按照日期分类的👇

前端刷题路-目录(日期分类)

经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇

前端刷题路-目录(题型分类)

有兴趣的也可以看看我的个人主页👇

Here is RZ