实现硬币分配问题:递归与动态规划详解

一、题目背景与问题描述

硬币换零钱是经典算法题,今天我们用Java来解决这样一个问题:

给定不同面值的硬币(1分、5分、10分、25分、50分),
求用这些硬币组成指定金额(1~50分)的所有组合数。

具体分组如下:

  • A组:只能用1分钱

  • B组:用1分和5分

  • C组:用1分、5分、10分

  • D组:用1分、5分、10分、25分

  • E组:用1分、5分、10分、25分、50分

举个例子,A组凑1分钱,只能用1个1分钱硬币;B组凑50分钱,可以用1分和5分多种组合。

条件

1.我们凑钱,可以全用最小单位凑,也可以找更大的单位来凑钱

2.再找更大的钱没有了,那就是终点

3.终止条件是凑够了对应的金额。

怎么才算一种情况呢

1.如果我凑50分凑到了49分,之后我往上加,要凑五分钱发现是54分,超过了,这不算一种情况

2.如果我凑50分,正好凑够,那这算一种情况。

二、递归

递归需要什么?

1.需要有递归的终止条件

2.找到递推公式

回归本题

本题中凑钱的终止条件是正好凑够,即为一种方法;或者超过目标金额,即,不是一种方法

递推公式为,每凑一次钱,可以用目前使用的钱种类再次凑钱,也可以用更大币值的钱去凑。

代码实现

public class Main {
    public static void main(String[] args) {
        int amount=50;
        int[]E={1,5,10,25,50};
        System.out.println(countWays(E,0,amount));
    
    }
    public static int countWays(int[]coins,int index,int amount){
        //我们先以凑够50分钱,用五种币来凑
        //递归首先有两个终止条件,如果复合,那么终止
        //否则,进行后续的递推公式
        
        //两种终止条件,当最后我们要凑的钱为0时候,即一种情况
        if (amount==0)return 1;

        //当钱是负数,或者说,我们要用更大的币值,但是这个币值它并不存在,那么就要0了。
        //有人会问 会不会有用到了更大币值同时剩余要凑的钱=0呢?
        //如果有这一步,那么上一步就是我们不用当前币值,用更大的币值
        //但是,这个过程并没有将要凑的钱减少,那么,上一步时候要凑的钱就是0
        //这个子问题,在上一步就终止了,return1了
        if (amount<0|| index==coins.length)return 0;

        //ok,终止条件写完,那么就该写递推公式了
        //凑钱1.用目前用到的币值凑,2.当前币值的币不用了,我用更大币值的币
        //最终呢,我会将所有的情况给包括进来
        //我会算到所有均用最小币值的去凑钱的return 1 也会得到所有用最大币值去凑钱的return
        //ok这就是全部流程
        int waysWithoutCurrent=countWays(coins,index+1,amount);
        int waysWithCurrent=countWays(coins,index,amount-coins[index]);
        return  waysWithCurrent+waysWithoutCurrent;
    }
}

我们改进一下算法,将所有答案均写出

public class Main {
    public static void main(String[] args) {
        int amount=50;
        int[]A={1};
        int[]B={1,5};
        int[]C={1,5,10};
        int[]D={1,5,10,25};
        int[]E={1,5,10,25,50};

        for (int i=0;i<=amount;i+=5)
            System.out.print(countWays(A,0,i)+" ");
        System.out.println();

        for (int i=0;i<=amount;i+=5)
            System.out.print(countWays(B,0,i)+" ");
        System.out.println();

        for (int i=0;i<=amount;i+=5)
            System.out.print(countWays(C,0,i)+" ");
        System.out.println();

        for (int i=0;i<=amount;i+=5)
            System.out.print(countWays(D,0,i)+" ");
        System.out.println();

        for (int i=0;i<=amount;i+=5)
            System.out.print(countWays(E,0,i)+" ");
        System.out.println();

    }
    public static int countWays(int[]coins,int index,int amount){
        if (amount==0)return 1;

        if (amount<0|| index==coins.length)return 0;

        int waysWithoutCurrent=countWays(coins,index+1,amount);
        int waysWithCurrent=countWays(coins,index,amount-coins[index]);

        return  waysWithCurrent+waysWithoutCurrent;
    }
}

运行结果如图

下面我们用递归去求两个解,为递归做铺垫

我们少一点,凑到0-50里面五的背书,同时只能用B:一分钱和五分钱

分别凑 5 10 15 20 25 30 35 40 45 50这十种钱 得到下面这个答案

分别凑1-50五十种答案得到下面的答案

ok,我们开始讲如何用递归写这道题

三、递归解题

1.定义问题        我们需要用B里面的一分钱和五分钱 去计算目标金额50 可以有的方法数
2.确定状态         定义一个一维数组来存储从0-50不同目标金额的问题的解
3.建立状态转移方程  一个50金额的方法数 怎么可以从子问题里去求它的解呢?我们可以用的是币值1分的和币值5分的钱去将能够加到50的子问题去加到50 
当我们用1分币的时候 凑够五十的问题是49的问题的解的个数+1

当我们用5分币的时候 凑够五十的问题是45的问题的解的个数+1
4.初始化状态 dp[0]=1
5.计算状态 从已知的基础知识开始,逐步计算出所有状态的值 用循环去实现
6.返回结果 从状态数组中抽取一个原问题的解,一般是最后一个值
7.验证    用特殊的用例来验证

我们用B同时目标值50来看答案,运行下列代码,答案为11。与上述结果相同

public class Main {
    public static void main(String[] args) {
        int amount=50;
        int[]B={1,5};

        System.out.println(coinDP(B,amount));

       
    }
 public static int coinDP(int [] coins,int amount){
        //记录不同目标金额可行解的答案
        int[]dp=new int[amount+1];

        dp[0]=1;//凑0分钱一种方法

        for (int coin:coins){
            //钱数组里面从小到大的每种钱使用时候
            //我们i的答案个数=i-coin的答案个数
            //遍历各种coin
            for (int i=coin;i<=amount;i++){
                dp[i]+=dp[i-coin];
            }
        }

            return dp[amount];
    }

}

四、总结

解这道题,我们首先想到的是递归,之后想到的是动态规划。

而效率高的是动态规划,效率怎么个高呢?我们会看两个的return;

递归return amount=50的结果是只能返回这个结果

而动态规划返回 amount=50的结果是有1-50这五十种结果,只是我们只需要返回50这个结果

递归适合问题初步解答以及小规模问题的解决,而动态规划适合大规模问题的解决

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值