题目链接:力扣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
提示:
看到这题应该可以立马想到是一道 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);
知道了这行代码这题就结束了
class Solution {
public:
int f = INT_MAX;
void dfs(vector<int> coins, int amount, int n, int m) {
if (m < 0 || n + amount / coins[m] >= f)
return ;
if (amount % coins[m] == 0) {
f = min(f, n + amount / coins[m]);
return ;
}
for (int i = amount / coins[m]; i >= 0; -- i) {
dfs(coins, amount - i * coins[m], n + i, m - 1);
}
}
int coinChange(vector<int>& coins, int amount) {
sort(coins.begin(), coins.end());
int n = 0;
dfs(coins, amount, 0, coins.size() - 1);
if (f == INT_MAX)
f = -1;
return f;
}
};
class Solution {
public:
vector<int> f;
int dp(vector<int> coins, int amount) {
if (amount < 0)
return -1;
if (amount == 0)
return 0;
if (f[amount - 1] != 0)
return f[amount - 1];
int t = INT_MAX;
for (int i = 0; i < coins.size(); ++ i) {
int m = dp(coins, amount - coins[i]);
if (m >= 0 && m < t) {
t = m + 1;
}
}
if (t == INT_MAX) {
f[amount - 1] = -1;
return -1;
}
else {
f[amount - 1] = t;
return t;
}
}
int coinChange(vector<int>& coins, int amount) {
if (amount < 1)
return 0;
f.resize(amount);
return dp(coins, amount);
}
};
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, amount + 1);
dp[0] = 0;
for (int i = 0; i < amount + 1; ++ i) {
for (int j = 0; j < coins.size(); ++ j) {
if (i >= coins[j]) {
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
if (dp[amount] > amount)
return -1;
return dp[amount];
}
};
动态规划的题不太好讲,一道题几乎就是一个转移方程,只要找到转移方程题目就解决了。对于找转移方程还需要多刷题,多练才比较容易找到。这道题还算是动态规划里的简单的题,题目算是非常直接的告诉你了。动态规划是一个比较难的算法,大家可以去看看y总的背包九讲,就在b站里。难的东西当然是要多练才行,而且动态规划是非常喜欢考的算法,一定要掌握。