一、题目背景与问题描述
硬币换零钱是经典算法题,今天我们用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这个结果
递归适合问题初步解答以及小规模问题的解决,而动态规划适合大规模问题的解决