动态规划——硬币找零思路

找零的两种问题

硬币找零问题,有两种。一种用贪心解决,一种用动态规划解决。
问题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的解决方法。

回溯法解决问题2

回溯法1

因为多次用了回溯法,所以很自然想到,每次决定一种硬币,使用一种数量,会产生什么状态。
例如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;
    }

}

回溯法2

我们还可以换一种思路。我们按照支付金额分阶段。
我们硬币种类有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...n1(F(Sci))+1 ,subject to S − c i &gt; = 0 S-c_i&gt;=0 Sci>=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<n;i++){
            if(coins[i]<=total){
                int  count = f(total-coins[i]);
                minCount = Math.min(count,minCount);
            }
        }
        return minCount==Integer.MAX_VALUE?-1:minCount+1;
    }
    public int findCoinCount(){
        return f(total);
    }
}

备忘录模式

基于上面的回溯画出这样的递归树。树中的每一个节点是一种状态,用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<n;i++){
            if(coins[i]<=total){
                int  count = f(total-coins[i],memory);
                minCount = Math.min(count,minCount);
            }
        }
        memory[total] =  minCount==Integer.MAX_VALUE?-1:minCount+1;
        return memory[total];
    }
    public int findCoinCount(){
        int[] memory = new int[total+1];
        return f(total,memory);
    }

}

动态规划

根据动态转移方程计算。

	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<coins.length;j++){
               if(coins[j]<=i){
                   dp[i] = Math.min(dp[i],dp[i-coins[j]]+1);
               }
           }
       }
       return dp[total]==max?-1:dp[total];
    }

放在最后的一点体会。将问题抽象为多阶段决策问题,要分清楚是按照什么分阶段。一般来讲是按照目标指标分阶段。例如支付9元最少需要多少个硬币,分阶段为支付8元最少需要多少个硬币,支付7元最少需要多少个硬币…。例如从(0,0)走到(n-1,n-1)最少需要多少步,目标是(n-1,n-1),前一步可以是(n-1,n-2)或者(n-2,n-1),知道他们的最少步数+1,就是目标值,所以可以按照到达的坐标分阶段。
按照某个指标做分阶段,回溯法枚举的时候不一定与此相关。回溯法枚举的时候,枚举的是选项。例如不同面值的硬币,不同的前进方向。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值