硬币找零问题,有两种。一种用贪心解决,一种用动态规划解决。
问题1:假设我们有 v1,v2,……,vn(单位是元)这些币值的硬币,它们的张数分别是 c1、c2、…, cn。我们现在要用这些钱来找零 w元,最少要用多少张纸币呢?
问题2:假设我们有几种不同币值的硬币 v1,v2,……,vn(单位是元)。如果我们要支付 w 元,求最少需要多少个硬币。比如,我们有 3 种不同的硬币,1 元、3 元、5 元,我们要支付 9 元,最少需要 3 个硬币(3 个 3 元的硬币)。
问题1有一个限制条件是不同面额的硬币的张数是有限制的,而问题2中没有这个限制。
问题1解决用贪心。选择小于w的,面值最大的硬币,来付钱。
问题2解决用动态规划。至于为什么不用贪心,我也不大明白。接下来重点描述问题2的解决方法。
因为多次用了回溯法,所以很自然想到,每次决定一种硬币,使用一种数量,会产生什么状态。
例如w=9,
在第0阶段,我可以决定1元,使用0张,产生一种状态:支付0元;1元,使用1张,产生一种状态:支付1元;1元,使用2张,产生一种状态:支付2元…
在第1阶段,我可以决定3元,使用0张,基于上一步的状态产生新的状态:支付0、1、2…元;使用1张,基于上一步的状态产生新的状态:1+3、2+3…
当支付金额等于9的时候,比较最少使用了多少个硬币。
public class CoinChange {
//钱币种类
private int[] coins = new int[]{1,3,5};
private int n = coins.length;
//总金额
private int total = 9;
private int minCount = Integer.MAX_VALUE;
private void f (int i,int coinCount, int sum){
if(sum==total){
minCount = Math.min(minCount,coinCount);
return;
}
if(sum>total){
return;
}
if(i==n){
return;
}
int maxCount = (total - sum)/coins[i];
System.out.println(maxCount);
for(int j=0;j<=maxCount;j++){
f(i+1,coinCount+j,sum+coins[i]*j);
}
}
public int findCoinCount(){
f(0,0,0);
return minCount;
}
}
我们还可以换一种思路。我们按照支付金额分阶段。
我们硬币种类有1 元、3 元、5 元。设F(9)表示想要支付9元最少需要几个硬币。F(9)可以从F(9-1)、F(9-3)、F(9-5)三个状态过来,分别在这三个状态+1,选择最低值就是F(9)最少需要支付多少个硬币。
定义状态转移公式:
F ( S ) = m i n i = 0 , 1 , 2... n − 1 ( F ( S − c i ) ) + 1 F(S)=min_{i=0,1,2...n-1}(F(S-c_i))+1 F(S)=mini=0,1,2...n−1(F(S−ci))+1 ,subject to S − c i > = 0 S-c_i>=0 S−ci>=0
F ( 0 ) = 0 F(0)=0 F(0)=0
F ( S ) = − 1 F(S)=-1 F(S)=−1,when n = 0 n=0 n=0
上一种回溯中是以每次选择一种硬币作为阶段。这次是每次支付够1元,2元,3元…n元,为阶段。
public class CoinChangeV3 {
//钱币种类
private int[] coins = new int[]{1,3,5};
private int n = coins.length;
//总金额
private int total = 9;
private int f (int total){
if(n==0) return -1;
if(total == 0) return 0;
int minCount = Integer.MAX_VALUE;
for(int i=0;i
基于上面的回溯画出这样的递归树。树中的每一个节点是一种状态,用f(total)表示。total表示当前方法要支付多少元。(这感觉不像是以前那种状态)我们看到(3). (5)都重复计算了很多次。可以采用备忘录模式,减少计算次数。
public class CoinChangeV3 {
//钱币种类
private int[] coins = new int[]{1,3,5};
private int n = coins.length;
//总金额
private int total = 9;
private int f (int total,int[] memory){
if(n==0) return -1;
if(total == 0) return 0;
if(memory[total] !=0) return memory[total];
int minCount = Integer.MAX_VALUE;
for(int i=0;i
根据动态转移方程计算。
public int findCoinCountDp(){
int max = total+1;
int[] dp = new int[total+1];
Arrays.fill(dp,max);
dp[0] = 0;
for(int i=1;i<=total;i++){
for(int j=0;i
放在最后的一点体会。将问题抽象为多阶段决策问题,要分清楚是按照什么分阶段。一般来讲是按照目标指标分阶段。例如支付9元最少需要多少个硬币,分阶段为支付8元最少需要多少个硬币,支付7元最少需要多少个硬币…。例如从(0,0)走到(n-1,n-1)最少需要多少步,目标是(n-1,n-1),前一步可以是(n-1,n-2)或者(n-2,n-1),知道他们的最少步数+1,就是目标值,所以可以按照到达的坐标分阶段。
按照某个指标做分阶段,回溯法枚举的时候不一定与此相关。回溯法枚举的时候,枚举的是选项。例如不同面值的硬币,不同的前进方向。