1. 问题描述:
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/coin-change
2. 思路分析:
① 通过题目可以初步知道,我们一开始是不清楚目标数字是如何通过给出的数组中的数字组成的,而对于这种不确定的解决方案的时候,使用dfs是可以解决的,因为它会尝试所有的可能性求解出符合要求的答案,这种题目也是可以使用dfs求解的典型问题,特点比较明显(不确定性),可能在一开始你会想到使用贪心的策略去解决,比如这样想:每一次都选择拿取最大面值去凑最后得到的结果就为答案了,实际上不是这样的,因为对应的测试数据不一样,所以有可能造成结果的偏差,例如最后几个数字都是相差不多,但是有可能比最后一个数字小一点的数字在组成目标数字的时候可能使用的硬币数量是最小的,因为可能就刚刚好凑成了,而选择最大的面值去凑的反而由于数据的某些差距导致使用的硬币数量多一点所以这个时候求解出来的答案是错误的
② 所以使用上面的贪心策略不完全正确,假如使用dfs来解决那么就要搜索所有的可能性了,这个时候才能判断出能够组成目标数字的方案中哪个使用的数量最少,一开始的时候想到使用数组中的面值一个个地去凑,对于小数据的话可以解决,假如对于大一点的数据执行耗时超过一秒,提交上去超时了,其实超时并不奇怪普通dfs耗时是比较大的,然后想到使用倍数的方法去凑,每一次都是使用面额最大的去凑,这样的话调用次数就会少很多,所以需要在for循环中递归需要里面再增加一层for循环表示当前面值的倍数情况,以最大倍数开始去凑一直到1倍数去凑,里面是有一个for循环是为了尝试一下当前面值的其他倍数有可能倍数小一点的凑的过程中是有的硬币数量比较少
③ 在递归的时候可以对其进行优化,一开始的时候没有剪枝使用倍数的方法去凑还是超时了,而且提交上去并没有快多少,后面对其进行了优化,在往下递归前判断是否大于了之前硬币数量,假如大于了那么就不用往下递归了因为结果都大了没啥作用,还有一个优化的点是将下面的pos参数修改为i - 1,其实这个优化也是很有作用的,因为以当前倍数去凑下一次递归的时候完全没有必要再以原来的位置进行递归了(因为都是以当前的最大倍数开始凑的),而是选择往前一个位置进行递归,假如下面两个的优化提交上去超时
④ 官方提供的动态规划 + 递归的代码也是值得学习的,它也是一个个去凑的知识在使用了数据来记录中间的求解过程了,核心是画出递归树:
③ 除了使用dfs方法之外,我们还可以使用另外的思路去解决,其实这道题目是完全背包的变形,我们可以借助于完全背包的思路来解决这个问题,其中dp[i]的含义是构成当前金额的使用的最少硬币的数量(dp数组的状态转移与当前面额与剩下面额是有关系的:理解清楚dp数组的状态转移方程)
3. 代码如下:
优化代码:
class Solution {
/*优化: 整除来进行兑换不要一个一个累加*/
public int coinChange(int[] coins, int amount) {
Arrays.sort(coins);
dfs(coins, amount, 0, coins.length - 1);
return res;
}
int res = -1;
private void dfs(int[] coins, int amount, int count, int pos) {
if (amount == 0){
if (res == -1 || res > count) res = count;
return;
}
for (int i = pos; i >= 0; --i){
for (int j = amount / coins[i]; j > 0 && (res == -1 || count + j < res) ; --j){
dfs(coins, amount - coins[i] * j, count + j, i - 1);
}
}
}
}
未优化之前的代码:
class Solution {
public int coinChange(int[] coins, int amount) {
Arrays.sort(coins);
dfs(coins, amount, 0, coins.length - 1);
return res;
}
int res = -1;
private void dfs(int[] coins, int amount, int count, int pos) {
if (amount == 0){
if (res == -1 || res > count)res = count;
return;
}
for (int i = pos; i >= 0; --i){
if (amount - coins[i] >= 0){
dfs(coins, amount - coins[i], count + 1, i);
}
}
}
}
官方代码:
public class Solution {
public int coinChange(int[] coins, int amount) {
if (amount < 1) return 0;
return coinChange(coins, amount, new int[amount]);
}
private int coinChange(int[] coins, int rem, int[] count) {
if (rem < 0) return -1;
if (rem == 0) return 0;
if (count[rem - 1] != 0) return count[rem - 1];
int min = Integer.MAX_VALUE;
for (int coin : coins) {
int res = coinChange(coins, rem - coin, count);
if (res >= 0 && res < min)
min = 1 + res;
}
count[rem - 1] = (min == Integer.MAX_VALUE) ? -1 : min;
return count[rem - 1];
}
}
完全背包:
from typing import List
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [-1] * (amount + 1)
dp[0] = 0
for i in range(amount + 1):
for coin in coins:
if i - coin >= 0 and dp[i - coin] != -1:
if dp[i] == -1 or dp[i - coin] + 1 < dp[i]: dp[i] = dp[i - coin] + 1
return dp[amount]