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,则无需使用递归。
- 我们假定,dp[i]的值为最大值时证明该金额无法使用现有的硬币凑齐;
- 对于金额i,使用不同面额coins[i]的硬币来凑,将问题F(i)化为子问题F(i-coins[i]);而由于自底向上的思想,子问题F(i-coins[i])必然先于F(i)先求解出来,直接取dp[i-coins[i]]即可;
- 对于使用不同面额的硬币,每次与前面的兑换策略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]; };
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理