详解动态规划最少硬币找零问题--JavaScript实现

转载来自https://juejin.im/post/5b0a8e0f51882538b2592963

看到一篇关于将找零钱问题的详解,写的通俗易懂,于是就搬运过来,一边自己理解。

硬币找零问题是动态规划的一个经典问题,其中最少硬币找零是一个变种,本篇将参照上一篇01背包问题的解题思路,来详细讲解一下最少硬币找零问题。如果你需要查看上一篇,可以点击下面链接: 详解动态规划01背包问题--JavaScript实现

 

下面让我们开始吧。

问题

给定4种面额的硬币1分,2分,5分,6分,如果要找11分的零钱,怎么做才能使得找的硬币数量总和最少。

分析

最少硬币找零问题,是为了求硬币的组合,所以一个大前提是硬币无限量供应。我们建立如下表格来分析问题:

 

 

其中每列用j表示零钱总额,每行i表示硬币面额。T[i][j]表示硬币个数,它是我们即将填入表格的数字。

在填写表格之前,我们需要先明确几个规则:

  • 当填写第i行时,使用的硬币面额仅能是i以及小于i的面额。举个例子,比如我填写第0行,i=0,那么这一样只能使用面额为1分的硬币。当我填写第2行,i=2,那么可以使用1分,2分,5分三种面额的硬币。
  • 当填写第j列时,表示当前需要使用硬币凑出的总额。比如j=6,表示需要使用硬币组合出总额为6分的情况。

1. i = 0

当我们只能使用面额为1分的硬币时,根据上面的规则,那么很显然,总额为几分,就需要几个硬币。即T[i][j] = j

 

 

 

2. i = 1

当我们有1分和2分两种面额时,那么组合方式就相对多了点。

i=1 j = 1:总额为1时,只能使用1分的面额。即填1。 i=1 j = 2:总额为2时,可以使用2个1分的,也可以使用1个2分的。因为我们要求最少硬币,所以使用1个2分的。表格所表达的意思是硬币的数量,所以这里也填1。 i=1 j = 3:总额为3时,可以使用3个1分的,也可以使用1个1分加1个2分。因此这里应该填2。 i=1 j = 4:总额为4时,可以使用4个1分的,可以使用2个1分加1个2分,也可以使用2个2分。其中硬币最少的情况应该是2个2分。因此这里填2。 i=1 j = 5:总额为5时,组合就更多了,但是聪明的你应该能想到使用2个2分加1个1分,可以实现最少硬币的需求。因此这里填3。

我们来看填写完上面5格后的情况:

 

 

建议你自己再纸上照着我这图画一个表格。接下来,别急着填表。我们要根据已有的数据,总结出T[i][j]的规律,然后通过填写剩余表格来验证。

我们将硬币面额使用数组coins[i]来表示,根据表格有 1分=coins[0], 2分=coins[1]。 当j<coins[i]时,T[i][j]的值,应该等于它的同列,上一行,即使T[i][j] == T[i-1][j]。 比如我们从表中所看到的,T[1][1]==T[0][1]。 当j>=coins[i]时,根据已有的 i=1行可以推出一个规律,令a = 1+T[i][j-coins[i]]T[i][j]= min(T[i-1][j],a),即二者比较取最小值。可能一开始你看到这个关于a的公式,有点太突然,难以接受。稍微解释一下,当第i行,优先选择这一样的硬币,因为这一行的硬币面额最大,最有可能使得总硬币数量最少。因此j-coins[i],就很好理解了,就是选择了这一行的硬币后,还剩下多少总额。举个例子,当i=1,j=3时,j-coins[1]=1。那么选择2分后,还剩余总额为1,这时候我们再定位到i=1,j=1,即T[1][1],它的值为1,再加上一个常数1,即得最终结果2。

再举例,i=1 j=5。由于是从左到右填表的,所以i=1,j<5的表格都填完了。j-coins[i]=3,定位到T[1][3]=2,加上常数1,即得最后结果T[1][5]=3

其实公式本身很短,也很好记。如果实在无法理解,建议先不用纠结。先最小化浏览器,不要看本篇剩余的内容。带着这个解题公式,自己在纸上,把这个表格填写完整,在填表分析的过程中就能慢慢理解了。

3. 剩余内容

按照上一步所提供的公式,其实所有的T[i][j]都可以填完了。如下表格。

 

 

 

建议先自己再纸上填表,填完了,再和我的图对比一下,看是否答案存在出入。

4.伪代码

以上的填表逻辑,使用伪代码表示如下


if(i == 0){
	T[i][j] = j/coins[i]; //硬币找零一定要有个 最小面额1,否则会无解
}else{
	if(j >= coins[i]){
		T[i][j] = min(T[i-1][j],1+T[i][j-coins[i]])
	
	}else{
		T[i][j] = T[i-1][j];
	}
}
复制代码

5. 寻找组合

至此,填完表格我们已经接近完成了。接下来要寻找从表格中寻找硬币组合。?

与填表顺序相反,寻找组合从有下角开始。

首先需要明确的是如果T[i][j] == T[i-1][j],那么就向上搜索。根据图来分析:

 

 

 

1. 定位到T[3][11] ,由于不存在T[i][j] == T[i-1][j],所以不用向上搜索,确定选中一个6分硬币。寻找组合的思路和填写T[i][j]的思路几乎是反过来的

2. 选择一个6分硬币后,剩余的总额为11-6=5。因此定位到T[3][5]中。由于T[3][5]==T[2][5],因此看图中的蓝色箭头,向上搜索,直到T[i][j] != T[i-1]

3. 定位到T[2][5]中,此时coins[i]为5分。选中5分硬币只有,剩余的总额为5-5=0。

4. 当j=0时,搜索结束。由上面步骤确定选中的硬币组合为:1个5分,1个6分。

代码

以上就是整个最少硬币找零问题的分析思路。最终代码使用 JavaScript 实现,如果你的 Sublime 支持纯 JavaScript,你可以直接复制黏贴代码,command + b 直接运行查看结果,然后修改输入变量,查看更多情况下的输出结果。

//动态规划 -- 硬币找零问题
function minCoins(coins,total,n){
	var T = [];

	for(let i = 0;i<n;i++){
		T[i] = []
		for (let j=0;j<= total;j++){
			if(j == 0){
				T[i][j] = 0;
				continue;
			}

			if(i == 0){
				T[i][j] = j/coins[i]; //硬币找零一定要有个 最小面额1,否则会无解
			}else{
				if(j >= coins[i]){
					T[i][j] = Math.min(T[i-1][j],1+T[i][j-coins[i]])
			
				}else{
					T[i][j] = T[i-1][j];
				}
			}

		}

	}
	findValue(coins,total,n,T);

	return T;

}

function findValue(coins,total,n,T){
	var i = n-1, j = total;
	while(i>0 && j >0){
		if(T[i][j]!=T[i-1][j]){
			//锁定位置,确定i,j值,开始找构成结果的硬币组合。 其实根据这种计算方法,只需要考虑最右边那一列,从下往上推。
			//console.log(T[i][j]);
			break
		}else{
			i--;
		}
	}

	var s = []; //存储组合结果
	
	while(i >= 0 && j > 0 ){
		
		s.push(coins[i]);
		j=j-coins[i];
		if(j <= 0){
			break; //计算结束,退出循环
		}
		//如果 i == 0,那么就在第 0 行一直循环计算,直到 j=0即可
		if(i>0){
			//console.log(i);
			while(T[i][j] == T[i-1][j]){
				i--;
				if(i== 0){
					break;
				}
			}
		}
	}
	
	console.log(s);
	//可以把数组s return 回去



}


var coins = [1,2,5,6];
var total = 11
var n = coins.length

console.log(minCoins(coins,total,n));


作者:YinTokey
链接:https://juejin.im/post/5b0a8e0f51882538b2592963
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我会尽力为您解答。首先,让我们来了解一下凑硬币问题。 在凑硬币问题中,我们需要最少硬币数来凑成给定的金额。例如,如果我们需要凑成 11 元,而有 1 元、2 元、5 元三种不同面值的硬币可供使用,那么最少需要使用 3 枚硬币,即 5 元、5 元和 1 元。 下面是一种使用动态规划解决凑硬币问题的方法: 1. 创建一个长度为 amount+1 的数组 dp,其中 dp[i] 表示凑成面值为 i 元所需的最少硬币数。 2. 初始化 dp[0] 为 0,其余元素初始化为一个较大的数(例如 amount+1),表示当前面值无法被凑成。 3. 遍历硬币数组 coins,对于每个硬币,遍历 dp 数组,更新 dp[j] 的值为 dp[j-coins[i]]+1,其中 j>=coins[i],表示当前面值可以被凑成。 4. 最终 dp[amount] 就是凑成 amount 元所需的最少硬币数,如果 dp[amount]>amount,则表示无法凑成给定金额。 下面是使用 JavaScript 实现该算法的代码: ```javascript function coinChange(coins, amount) { const dp = new Array(amount + 1).fill(amount + 1); dp[0] = 0; for (let i = 0; i < coins.length; i++) { for (let j = coins[i]; j <= amount; j++) { dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1); } } return dp[amount] > amount ? -1 : dp[amount]; } ``` 这个函数接受两个参数,一个是硬币数组 coins,另一个是需要凑成的金额 amount。最终返回凑成 amount 元所需的最少硬币数,如果无法凑成则返回 -1。 例如,若 coins = [1, 2, 5],amount = 11,则 coinChange(coins, amount) 的返回值为 3,表示凑成 11 元最少需要 3 枚硬币。 希望这个简单的解释和代码能够帮到您,如果您还有其他问题,请随时问我。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值