前端刷题路-Day26:零钱兑换 I(题号322)

274 阅读4分钟

零钱兑换(题号322)

题目

给定不同面额的硬币 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] <= 231 - 1
  • 0 <= amount <= 104

链接

leetcode-cn.com/problems/co…

解释

这题自己并没有想出答案,感觉思维被限制住了,一直的想法就是:

先用面额最大的零钱去除总数,取余之后换用下一个面额的零钱,如果面额小于余数,就使用在上一步减去1,增大余数,或者换用更小面额的零钱,以此类推,得出最后的答案。

后来看了题解之后发现这其实就是贪心算法,不过我这里没有进行剪枝操作,如果数组或者数字过大会出现超时的情况。

其实这题的正经答案很简单,并且是之前遇到过的。

对于这一题可以类比成之前遇到过的爬楼梯,一次可以爬一个台阶或者两个台阶,那爬N个台阶有多少种可能。可即使知道了这种思路也没有成功写出代码,因为对这块的理解不够深刻。

在处理台阶问题时,一直以为是斐波那契数列的原因,其实并不是。当前的DP方程是:

DP[i] = DP[i-1] + DP[i-2]

DP数组里存储的是走到这一步的可能性,这里的i-1i-2并非是我想象中的斐波那契数列,而是可以走一步或者走两步,如果这题改成了可以走一阶、二阶、三阶,那么这里的DP方程就应该改成:

DP[i] = DP[i-1] + DP[i-2] + DP[i-3]

以此类推,这种题目应该推导到i-nn代表着可以走的阶数。

放到这题里也是同理,只可惜当时没想到。

自己的答案

更好的方法(动态规划)

先说下思路,依然是经典的从后往前想。

假设我们目标金额是100块,而硬币的面值有10块,20, 5块。

从后往前想,最后一步的可能性为:

  • 手里有95块:95块时最少的硬币数,加上1(5块) ,也就是dp[95] + 1
  • 手里有90块:90块时最少的硬币数,加上1(10块) ,也就是dp[90] + 1
  • 手里有80块:80块时最少的硬币数,加上1(20块) ,也就是dp[80] + 1

那么最后的结果就是上面三种可能性中硬币数量最少的一种,那么继续往前推理,95块的时候也有三种可能,分别是90块、85块、和75块,那么之后再往前推理,可以拿到最开始答案。

那么此时就可以从前往后推理了,每次取当前数减去不同硬币面额的最小值加1即可拿到最终的结果。

代码👇:

var coinChange = function(coins, amount) {
  var dp = new Array(amount + 1).fill(Infinity)
  dp[0] = 0
  for (var i = 1; i < amount + 1; i++) {
    for (var coin of coins) {
      dp[i] = coin <= i ? Math.min(dp[i], dp[i - coin] + 1) : dp[i]
    }
  }
  return dp[amount] === Infinity ? -1 : dp[amount]
};

动态规划就很经典了,不过需要注意的是这里的数组长度是amount+1

原因很简单,数组是从0开始增加的,如果长度为amount,则会少一位,所以长度需要比amount多1。

接下来就是正常的循环,首先是对数字进行循环,这一点无需多言,接下来就对数组进行循环,拿到每个硬币的面额,取减去面额前的金额的最少硬币数加1,这就完事了。

最后的return是用来判断当硬币无法组合出金额的情况,不一定非得用Infinity,用别的标示也是一样的。

更好的方法(贪心+剪枝)

其实这题用贪心也能解决的,只是需要看你剪枝剪得怎么样。

贪心的原理在解释部分也说过了,只是把它转化成代码是比较困难的一步,因为条件又点多。

同时需要注意剪枝,如果当前的组合的硬币数目比最小值大,那就果断放弃,直接看代码👇:

var coinChange1 = function(coins, amount, res = Infinity) {
  coins.sort((a, b) => b - a)
  var d = (amount, index, count) => {
      if (amount === 0) return res = Math.min(res, count)
      if (index === coins.length) return
      for (var n = amount / coins[index] | 0; count + n < res && n >= 0; n--) 
          d(amount - n * coins[index], index + 1, count + n)
  }
  return d(amount, 0, 0), res === Infinity ? -1 : res
};

需要注意的就是for循环里面的条件的:

count + n < res && n >= 0

第一个条件是必须小于最小值,第二个条件是当前金额的硬币数需要大于0。

不过👆的代码在LeetCode添加测试用例之后就不好使了,测试用例太大会导致贪心算法的超时,这里看看思路就好了,剪枝剪得确实很强的啊,原文地址



PS:想查看往期文章和题目可以点击下面的链接:

这里是按照日期分类的👇

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

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

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

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

Here is RZ