找零问题,四种方法
- 1、贪心算法
贪心算法的核心思想就是只取眼前的最佳值,不从整体看而是遵从局部最优选择,因此贪心算法不一定能得出全局最优解。
/**
* 1、贪心算法:获取零钱数量的数组,其元素为不同面值类前所需的数量
* @param total 总金额
* @param changes 零钱面值表,要逆序排列(即从大到小)
* @return 返回一个零钱数量数组
*/
public static int[] getChangeByGreddy(int total, int[] changes) {
int[] changeDetail = new int[changes.length]; // 记录不同面值的零钱要找多少张
Arrays.fill(changeDetail, 0);
for (int i = 0; i < changes.length; i++) {
changeDetail[i] = total / changes[i];
total -= changes[i] * changeDetail[i];
if (total == 0) return changeDetail;
}
return null;
}
代码执行结果如下:
total=155
changes[]={15, 10, 5}
输入总金额:
155
需要面值 15的数量为:10
需要面值 10的数量为:0
需要面值 5的数量为:1
贪心算法结果: 11
- 2、递归:自顶向下,类似树形结构,要完成递归必须具备两个限制条件:
1)终止递归的限制条件
2)越来越接近终止条件
/**
* 2、递归:获取找零数量,调用findMinAmount递归方法
* @param total 总金额
* @param changes 零钱面值表
* @return 返回要找的零钱数量
*/
public static int getChangeByRecursion(int total, int[] changes) {
if (changes.length == 0) return -1;
findMinAmount(total, changes, 0);
if (result == Integer.MAX_VALUE) return -1; // 不能找零返回-1
else {
return result; // result是一个全局变量,代表最终结果
}
}
/**
* 递归方法,在getChangeByRecursion中被调用,该方法目的是为了找到total金额可以兑换的最少零钱数量
* @param remain 剩余的金额(最开始为总金额total,在递归中不断减少)
* @param changes 零钱面值表
* @param count 所需零钱的数量
*/
public static void findMinAmount(int remain, int[] changes, int count) {
if (remain < 0) return; // 递归的第一个限制条件:total<0时递归停止
if (remain == 0) result = Math.min(result, count);
for (int i = 0; i < changes.length; i++) {
findMinAmount(remain - changes[i], changes, count + 1); // 第二个限制条件:每次递归调用之后会越来越接近第一个限制条件,即total越来越小
}
}
代码执行结果如下:
total=20
changes[]={15, 10, 5}
输入总金额:
20
递归结果: 2
递归过程的树状图如下:
- 3、记忆化搜索:自顶向下,对于可能出现的重复计算,使用一个数组来保存其值,在进行重复的计算的时候,就可以直接调用数组中的值,减少不必要的递归。
/**
* 3、记忆化搜索,调用递归方法findMinAmount2,比方法2直接递归快很多
* @param total 当前的金额
* @param changes 零钱面值表
* @param count 所需零钱的数量
* @return 返回findMinAmount2的结果,即最小零钱数
* 时间复杂度:O(total*changes.length)
* 空间复杂度:O(total)
*/
public static int getChangeByMemory(int total, int[] changes) {
if (total < 1) return 0;
return findMinAmount2(total, changes, new int[total]); // 调用递归方法
}
/**
* 递归方法,在getChangeByMemory中被调用
* @param remain 剩余的金额(最初为总金额total,在递归中不断减少)
* @param changes 零钱面值表
* @param counts 不同金额所需零钱数的最小值数组,counts[total-1]代表total金额所需最小零钱数,记忆化搜索的特点在此
* @return 某金额(<=total)所需最小零钱数
*/
public static int findMinAmount2(int remain, int[] changes, int[] counts) {
if (remain < 0) return -1;
else if (remain == 0) {
return 0;
}
else if (counts[remain-1] != 0){ // remain金额的最小零钱数不为零则直接返回该数,不再执行后面的递归
return counts[remain-1];
}
//若counts[remain]不存在,则继续下面的步骤给counts[remain]赋值
int min = Integer.MAX_VALUE; // 最小零钱数
for (int change : changes) {
int minNumber = findMinAmount2(remain - change, changes, counts); // 递归
if (minNumber < min && minNumber >= 0) min = minNumber + 1; // 加一是算上total-change中change面值的一张数量
}
counts[remain-1] = min;
return counts[remain-1];
}
代码执行结果如下:
输入总金额:
155
记忆化搜索结果: 11
- 4、动态规划:自底向上,记忆化搜索是从counts[total-1] 开始,而动态规划从counts[1] 开始,counts[i] 由counts[1] ~counts[i-1] 决定,根据局部的最优解获得整体的最优解。
采用自下而上的方式进行思考,定义 Counts(i) 为组成金额 i 所需最少零钱数,changes[j] 代表零钱数组中第j+1个零钱面值,Counts(i) 对应的状态转移方程如下:
C o u n t s ( i ) = m i n j = 0... n − 1 ( C o u n t s ( i − c h a n g e s [ j ] ) ) + 1 (1) Counts(i)= min_{j=0...n-1}(Counts(i-changes[j]))+1 \tag{1} Counts(i)=minj=0...n−1(Counts(i−changes[j]))+1(1)
/**
* 4、动态规划
* @param total 当前的金额
* @param changes 零钱面值表
* @return 返回最小零钱数
* 时间复杂度:O(total*changes.length)
* 空间复杂度:O(total)
*/
public static int getChangeByDp(int total, int[] changes) {
if (total < 1) return -1;
int max = total;
int[] counts = new int[total+1]; // 存储0~total所有金额分别所需的最小零钱数的数组
Arrays.fill(counts, max); // 给counts的所有元素设置一个最大初始值
counts[0] = 0;
// for(i)确定counts[i]
for (int i = 1; i <= total; i++) {
// for(j)遍历changes[],找到能给金额i找零且所需零钱数最小的面值,并将零钱数赋给counts[i]
for (int j = 0; j < changes.length; j++) {
if (changes[j] <= i) {
/*
* 将金额i拆分为i-changes[j]+changes[j],将该式子转换为对应的零钱数量:counts[i-changes[j]]+1,并与counts[i]比较,取较小值
* counts[i-changes[j]]是子问题的结果,即小于金额i的其他金额所需最小零钱数,加一代表的是changes[j]的那一张零钱
*/
counts[i] = Math.min(counts[i], counts[i-changes[j]]+1);
}
}
}
return counts[total] > max ? -1 : counts[total]; // 若counts[total]比初始值小,返回该值为最终结果,否则返回-1
}
代码执行结果如下:
total=10
changes[]={15, 10, 5, 1}
输入总金额:
10
动态规划结果: 1
具体执行过程如下图:
参考文章:
[1] https://leetcode.cn/problems/coin-change/solutions/137661/javadi-gui-ji-yi-hua-sou-suo-dong-tai-gui-hua-by-s/
[2] https://leetcode.cn/problems/coin-change/solutions/132979/322-ling-qian-dui-huan-by-leetcode-solution/