322. 零钱兑换

 


题目描述

  给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。你可以认为每种硬币的数量是无限的。

 

解题思路

错误思路

  一开始想到的是贪心算法,将不同面额的硬币从大到小排序后,尽可能地取较多个面额大的硬币,剩下的金额依次取较大面额的硬币(若硬币面额大于剩下金额则跳过)直到达到总金额为止,若其余硬币的面额换不到剩下面额则返回-1。依次以面额coins[i]的硬币开头,表示此次兑换的最大面额为coins[i],最后比较以不同面额硬币开头的各次兑换,谁用的硬币越少谁最优。但这种思路是错误的,每次兑换轮到某个面额coins[i]作为兑换的最大面额时,我们都是尽可能地多拿,但有可能我们少拿一个该面额的硬币就能达到最优解,而这种方式使用贪心算法是做不到的。

复制代码
示例:
coins = [5,3,2],amount = 11

以最大面额为5的硬币开头:11=5*2+1,但没有面额为1的硬币,因此返回-1;
以最大面额为3的硬币开头:11=3*3+2,需要4个硬币
以最大面额为2的硬币开头:11=2*5+1,同样没有面额为1的硬币

因此用贪心法求得最优解为4

实际上,我们在以最大面额为5的硬币开头进行兑换时,可以这样11=5*1+3*2,需要3个硬币,这才是最优解
复制代码

  

  改进:以面额为coins[i]的硬币开头时,求得能使用面额为coins[i]的硬币的最多个数为num =  amount/coins[i] , 内层使用循环,依次比较使用面额为coins[i]的硬币个数从0个至num个时,各个兑换策略的硬币个数,得到最小值。外层循环则比较以不同面额硬币开头的各次兑换策略,得到最终最少的硬币个数。但该方法超时。

 

部分代码如下:

复制代码
function dfs(p,ticket,index,count){
  if(p===0){
    return count;
  }
  if(index>=ticket.length&&p>0){
    return Number.MAX_VALUE;
  }
  let num = Math.floor(p/ticket[index]);
  let min = Number.MAX_VALUE;
  for(let i=0;i<=num;i++){
    let pp = p-ticket[index]*i;
    min = Math.min(dfs(pp,ticket,index+1,count+i),min);
  }
  return min;
}
复制代码

 

正确解法

 

 

 

  如上图所示,每使用一个面额为coins[i]的硬币,相当于把问题F(amount)化为子问题F(amount-coins[i]),此时变成求解凑成总金额为amount-coins[i]所需的最少硬币数,而问题 F(amount) = F(amount-coins[i])  + 1 。超时的主要原因是使用了太多层的递归嵌套,造成重复计算某个金额的值,我们使用记忆化深度优先搜索,保存计算过的不同金额即可。另外,也无需排序硬币面额。

 

复制代码
/**
 * @param {number[]} coins
 * @param {number} amount
 * @return {number}
 */
var coinChange = function(coins, amount) {
    if(amount===0){
        return 0;
    }
    const minCount = new Array(amount).fill(0);//存储1~amount各金额所需的最少硬币数
    function dfs(coins,amount,minCount){
        if(amount<0){
            return -1;
        }
        if(amount===0){
            return 0;
        }
        if(minCount[amount-1]!==0){
            return minCount[amount-1];
        }
        let min = Number.MAX_VALUE;
        for(let coin of coins){
            let res = dfs(coins,amount-coin,minCount); 
            if(res>=0){
                min = Math.min(min,res+1);
            }
        }
        
        minCount[amount-1] = min===Number.MAX_VALUE?-1:min;
        return  minCount[amount-1];    
    }
    let ans = dfs(coins,amount,minCount);
    ans = ans===Number.MAX_VALUE?-1:ans;
    return ans;
};
复制代码

 

改进:

  上面使用深度优先搜索,是从问题F(amount)逐步分解为子问题,直至变成问题F(1)或F(0),是自顶向下的思想。那么,使用自底向上的思想,从金额amount=1开始计算,一直计算到总金额amount,则无需使用递归。

  1. 我们假定,dp[i]的值为最大值时证明该金额无法使用现有的硬币凑齐;
  2. 对于金额i,使用不同面额coins[i]的硬币来凑,将问题F(i)化为子问题F(i-coins[i]);而由于自底向上的思想,子问题F(i-coins[i])必然先于F(i)先求解出来,直接取dp[i-coins[i]]即可;
  3. 对于使用不同面额的硬币,每次与前面的兑换策略dp[i]进行比较,取较小值作为新的dp[i];

 

示例:
coins = [1,2,3], amount=6

 

过程如下:

 

 

代码如下:

复制代码
/**
 * @param {number[]} coins
 * @param {number} amount
 * @return {number}
 */
var coinChange = function(coins, amount) {
    let min = amount+1;//由于面值最小为1,因此最多的硬币个数可以设为amount+1
    const dp = new Array(amount+1).fill(min);//初始化各个金额的所需硬币最少个数
    dp[0] = 0;//对应amount = 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);//化为子问题F(i-coins[j])
            }
        }
    }
    return dp[amount]>amount?-1:dp[amount];

};
复制代码

 

posted @   ˙鲨鱼辣椒ゝ  阅读(49)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示