动态规划思想解决找零问题,最精炼的实现(JavaScript版)

1,背景:

十一节前练习了使用动态规划、回溯、贪心等算法实现找零问题:

动态规划、回溯、贪心算法解找零问题(最小张数付款问题)(JavaScript实现)_yangxinxiang84的专栏-CSDN博客

是实现出来了,也没有问题,但是总感觉使用动态规划实现的时候状态转移部分有点奇怪,有点复杂,理解上有一些困难。遂向大神川哥请教,大神就是大神,咔咔咔一会给我发了一个Java版本过来,当看到川哥的实现的代码的时候,我以为他理解错了(怀疑实现错了),因为代码确实太精炼了,居然没有用除法,没有用取模。。。

当我将代码翻译成JS版本,测试了很多个场景后,发现都没问题,大神就是大神。这是我目前看到过的所有找零需求实现中最精炼的一个版本:

2,需求和实现:

我们有n种不同面值的硬币,需要支付n元(整数),最少需要多少个硬币?

比如具体的:我们有 3 种不同面值的硬币,11 元、5 元、1 元,我们要支付 15 元(整数),最少需要 多少个硬币?假设每种硬币都有无数个,想用多少用多少,哈哈。

最少要3 个硬币(3 个 5 元的硬币)。

直接上代码:

/**
 * 我们采用自下而上的方式进行思考
 * 定义F(i)为组成金额 i 所需最少的硬币数量。 初始化的时候,F(i)初始化为一个很大的数(大于需要找零的钱即可)
 * 假设在计算F(i)之前,我们已经计算出F(0) 到F(i-1)的答案。则F(i)对应的状态转移方程应为:
 * F(i) = min( F(i), F(i-Cj) + 1 );  j=0->n-1; n=可用硬币面值类型数
 * 也就是,第 i 块钱的时候,要分别尝试去使用每一个可用面值的钱一次(i-coin),此时使用过一次硬币,数量要+1,因此状态转移方程如上所示。
 * 其中Cj代表的是第 j 枚硬币的面值,即我们枚举最后一枚硬币面额是Cj
 * 那么需要从 i-Cj 这个金额的状态 F(i-Cj) 转移过来
 * 再算上枚举的这枚硬币数量1的贡献
 * 由于要硬币数量最少,所以 F(i) 为前面能转移过来的状态的最小值加上枚举的硬币数量 1 
 * @param {*} coins 各种面值硬币数量 
 * @param {*} amount 需要凑的钱数
 * @returns 
 */
function coin2Change(coins = [11, 5, 1], amount = 15) {
  // 最大值假设就是要求和的数量加上1,这个一定是最大的,再多的硬币也就是1分的全部构成
  const max = amount +1;
  // 初始化dp数组,初始化到大小为金额大小再加1,因为dp的第一个元素是0
  const dp = new Array(amount +1)
  dp.fill(max)
  dp[0] = 0;
  // 外层循环就是构建F(0)到F(i)的过程
  for(let i = 1; i<=amount; i++){
    // 内层循环是遍历整个零钱数组的过程:当前需要凑的钱数 i 都尝试一下所有可用面值的硬币
    for(const coin of coins) {
      // 只有当前的零钱比当前的总额 i 小,才有可能被加上,超过了直接下一个
      if(coin <= i){
        // 当前 F(i)的局部最优解,一定是当前 F(i) 和F(i - 当前面值) + 1里面比较小的一个
        dp[i] = Math.min(dp[i], dp[i-coin] + 1)
      }
    }
  }
  // 有可能凑不足需要的钱,返回 -1
  return dp[amount] > amount ? -1 : dp[amount] ;
}
// 测试
const amount = 21
const coins = [11, 5, 1]
const rst = coin2Change(coins, amount);
console.log(`call coin2Change end, rst = ${rst}`);

3,难点分析

这个需求,代码很短,注释也比较详尽,但是精炼的代码有时候理解上去更困难,需要理解这种实现背后的方法、思想。我前后花了一天多的时间,算是理解了,这个算法的难点是理解状态转移方程:

F(i) = min( F(i), F(i-Cj) + 1 );  j=0->n-1; n=可用硬币面值类型数

动态规划就是数学的归纳演绎,确实比较抽象!这里的思路是:假设在计算F(需要找零的钱)之前,我们已经计算出F(0) 到F(需要找零的钱-1)的值。dp就是F哈,代表的是状态:

用一维数组dp[]来记录状态,dp[] 中每个元素默认是一个很大的数(比要找零的钱大即可,也就是这代码中的const max = amount +1;),dp[0] = 0,也就是找0块钱的时候,需要0个硬币,dp[需要找零的钱] 含义是凑足“需要找零的钱”所需要的最小硬币数。

dp[需要找零的钱] = min( dp[需要找零的钱] , dp[需要找零的钱 - 当前使用硬币的面值] + 1),从1单位(元)开始,逐一计算到当前面值,每一个需要找零的钱数都去尝试使用每一种可用的硬币,取最小值。F(i-Cj) + 1指的是 : 当前需要找零的钱所需硬币数,等于“前”一个需要找的钱硬币数F(i-Cj) “前”一个指的事:需要找零的钱减去当前硬币面值)加上1,加1是因为使用了1次该面值的硬币了。

附上川哥原稿:

4,动态规划递归版实现

理解了上面的思路之后,使用递归的方案来实现就比较简单了,撸一遍代码:

(我理解的就是递归,但有点把它叫做递归版动态规划)

/**
 * 递归动态规划实现找零需求
 * @param {*} coins , 可用的币种数组,每种币种梳理无限制。
 * @param {*} amount , 需要凑足的钱数
 * @returns totalMin, -1 或者凑足需要找零 amount 所需的最少硬币数
 */
function coin2ChangeR(coins, amount){
  // 再包装一下,主要是处理不满足条件的场景。同时做备忘录缓存
  const cache = new Map(); 
  // 递归动态规划实现找零 
  function change(coins, amount){
    if(amount === 0){
      return 0;
    }
    // 备忘录缓存优化
    if(cache.get(amount)){
      return cache.get(amount);
    }
    const counts =[];
    // 对当前的amount, 每一个都尝试使用一下所有面值币种硬币,然后取最小值
    for(const coin of coins){
      if(coin <= amount) {
        const count = change(coins, amount - coin) +1;
        counts.push(count)
      }
    } 
    const minCount = Math.min(...counts);
    cache.set(amount, minCount)
    return minCount
  }
  const totalMin = change(coins, amount)
  return totalMin > amount ? -1 : totalMin  
}
// 测试一下
const amount = 10
const coins = [11, 6, 3]
const rstR = coin2ChangeR(coins, amount);
console.log(`coin2ChangeR :: call end, rstR = ${rstR}`);

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值