题目
题目链接:力扣322. 零钱兑换
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
提示:
- 1 <= coins.length <= 12
- 1 <= coins[i] <= 231 - 1
- 0 <= amount <= 104
解题思路
看到这题应该可以立马想到是一道 dp
题,如果没怎么接触过 dp
,估计一时半会做不出来。那就先从会的思路开始想。
👉暴力递归(会超时)
没思路的最好办法就是暴力,先暴力出来再想优化。
这题我的第一思路是,先把硬币排序,然后再从大到小开始遍历,对总金额 amount
取余再整除,得到硬币数。但发现样例是:coins = [2, 3], amount = 4
的时候就出错了。所以必须遍历所有情况。
如何遍历所有情况?
递归,把硬币从大到小,取 [0, amount / coin[i]]
范围的个数,计算出最少的硬币个数。把范围内的硬币个数全都遍历一遍即可。递归的深度是硬币数组的长度(如果出现可以直接被总金额整除的硬币,那么可以直接返回,因为遍历是从硬币面值大开始的),递归的宽度是总金额能被当前硬币面额整除的最大硬币数。
递归的深度:从递归开始到第一次 return 的长度
递归的宽度:递归函数里的循环的长度
具体做法:
-
先把硬币数组排序,再进入递归
-
递归函数的返回条件:
- 递归是从硬币面值大的到小的,所以就是遍历硬币数组,所以数组下标不能小于
0
- 如果递归到当前层次时,硬币的总个数大于之前已经存储的个数,那么这里的递归一定不是最少的硬币个数,可以直接返回
- 如果发现当前硬币面额可以整除当前总金额,判断和已存储的个数的大小,取小的那个。然后返回
- 递归是从硬币面值大的到小的,所以就是遍历硬币数组,所以数组下标不能小于
-
递归函数的参数:
- 硬币数组
- 总金额
- 硬币总个数
- 硬币数组的下标
-
递归函数的实现功能
循环遍历当前面额的硬币可以取的个数(
[0, amount/coins[i]]
)
👉优化
上面的方法超时了,原因在于有太多的重复的计算,比如:coins = [1, 2, 3], amount = 7
,当遍历 3
时,后面会遍历 1
和 2
。遍历 2
时,会遍历 1
。这样就出现了重复的情况,可以当 amount
减少一次,就把其对应的所需硬币数存储起来,下次遇到时可以直接使用。
这需要把上面的方法稍微改一下。递归的深度是总金额的大小,递归的宽度是硬币数组的长度。
-
递归函数的参数
- 硬币数组
- 总金额
-
递归函数的返回条件
- 当总金额小于
0
时,返回-1
- 当总金额等于
0
时,返回0
- 当存在当前情况存储的硬币数时,返回存储的硬币数
- 当总金额小于
-
递归函数的实现功能
循环遍历硬币数组,将总金额减去硬币面值,放入递归中,取所有的返回值中最小的那个。然后存入数组中
这种方法解决了重复计算所以就没有超时,但不是最优的。
👉动态规划
动态规划感觉就是对上面优化的优化吧。
先给出转移方程吧:F[i] = min(F[i - coins[j]]) + 1,其中 j
的取值范围是 coins
数组的全范围。
因为动态规划都是先从下标小的到下标大的,下标大的需要使用下标小的数组值。所以需要先从开始计算,直到最终。
令 F[0] = 0
。F[i]
的取值与 coins
数组和 amount
值有关,所以直接给出核心代码吧:dp[i] = min(dp[i], dp[i - coins[j]] + 1);
知道了这行代码这题就结束了
代码(C++)
递归
class Solution {
public:
int f = INT_MAX;
void dfs(vector<int