最少的硬币数目

1,309 阅读1分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

一、题目描述:

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

输入: coins = [1, 5, 10], amount = 15
输出: 3

二、思路分析:

根据生活常识我们知道人民币的面值是 [1, 5, 10, 20, 50, 100] 。需要凑出某个金额,需要用尽量少的钞票,很显然的策略是能用大的尽量用大的。

var coinChange = function(coins, amount) {
  const RMB = coins.sort((a, b) => b - a)
  // 先排序钞票,大的在前面小的在后面,能用大的尽量用大的
  let count = 0
  let current = amount
  for (let i = 0; i < RMB.length; i++) {
    let use = Math.floor(current / RMB[i])
    count += use
    current -= RMB[i] * use
  }
  return count
}

const RMB = [100, 50, 20, 10, 5, 1]
console.log(coinChange(RMB, 666)) // 10

这种算法我们称为贪心算法,特点就是鼠目寸光,能大则大。但是,如果我们换一组钞票的面值,贪心策略就也许不成立了。如果一个奇葩国家的钞票面额分别是 [1, 5, 11],那么我们在凑出15的时候,贪心策略会出错: 

15=1×11+4×1 (贪心策略使用了5张钞票)
15=3×5 (正确的策略,只用3张钞票)

代码好好的,到我这里就运行不了了,原来是算法出错了,贪心算法没有考虑全局最有解,而是局部最优解。接下来来分析全局最优解如何计算。

已知面值 [1, 5, 11] ,凑出n需要多少张钞票。假设计算f(n)之前我们以及计算了f(0)到f(n-1)的答案,也就是已经知道凑14需要多少张,凑13需要多少张等等。

让我们从规模最小的n开始。

当n=0时,即我们需要多少个币来凑够0元呢? 由于1,5,11都大于0,即没有比0小的币值,因此凑够0元我们最少需要0个币。于是我们就得到了dp[0]=0, 表示凑够0元最小需要0个硬币。

当n=1时,只有面值为1元的硬币可用, 因此我们拿起一个面值为1的币值,接下来只需要凑够0元即可,而这个是已经知道的dp[0]=0。所以,dp[1]=dp[1-1]+1=dp[0]+1=0+1=1

当n=2时, 仍然只有面值为1的硬币可用,于是我拿起一个面值为1的硬币, 接下来我只需要再凑够2-1=1元即可(记得要用最小的硬币数量),而这个答案也已经知道了。 所以dp[2]=dp[2-1]+1=dp[1]+1=1+1=2。

...

当n=5时,只有面值为5元的硬币可用,因此我们拿起一个面值为5的币值,接下来只需要凑够0元即可,而这个是已经知道的dp[0]=0。所以,dp[5]=dp[5-5]+1=dp[0]+1=0+1=1

当n=6时,我们能用的硬币就有两种了:1元的和5元的( 10元的仍然没用,面值太大了)。 既然能用的硬币有两种,我就有两种方案。如果我拿了一个1元的硬币,我的目标就变为了: 凑够6-1=5元需要的最少硬币数量。即dp[6]=dp[6-1]+1=dp[5]+1=1+1=2。第二种方案是我拿起一个5元的硬币, 我的目标就变成:凑够6-5=1元需要的最少张纸币。即dp[6]=dp[6-5]+1=dp[1]+1=1+1=2。

当n=6时,我们需要计算

Max.min(dp[6-1],dp[6-5],dp[6-11]) + 1
= Max.min(dp[5], dp[1], 0) + 1
= Max.min(1, 1) + 1
= 2

dp[6-11]表示凑6元的时候11元的仍然没用,面值太大了。

function coinChange (coins, amount) {
  const Max = amount + 1
  const dp = new Array(amount + 1)
  dp.fill(Max)
  dp[0] = 0
  for (let i = 1; i <= amount; i++) {
    for (let j = 0; j < coins.length; j++) {
      if (coins[j] <= i) {
        dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1)
      }
    }
  }
  return dp[amount] > amount ? -1 : dp[amount]
}
console.log(coinChange([1, 5, 11], 15)) // 3种

四、总结:

动态规划(dynamic programming,DP)是求解决策过程(decision process)最优化的数学方法。实现的过程会相对复杂。把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。