给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。
https://leetcode-cn.com/problems/coin-change
动态规划
对于求极值问题,主要考虑动态规划 Dynamic Programming 来做,好处是保留了一些中间状态的计算值,可以避免大量的重复计算。
我们维护一个一维动态数组 dp,其中 dp[i] 表示钱数为i时的最小硬币数的找零,注意由于数组是从0开始的,所以要多申请一位,数组大小为 amount+1,这样最终结果就可以保存在 dp[amount] 中了。
初始化 dp[0] = 0,因为目标值若为0时,就不需要硬币了。其他值可以初始化是 amount+1,为啥呢?
因为最小的硬币是1,所以 amount 最多需要 amount 个硬币,amount+1 也就相当于当前的最大值了,注意这里不能用整型最大值来初始化,因为在后面的状态转移方程有加1的操作,有可能会溢出,除非你先减个1。
更新 dp[i] 的方法就是遍历每个硬币,如果遍历到的硬币值小于i值(比如不能用值为5的硬币去更新 dp[3])时,用 dp[i - coins[j]] + 1 来更新 dp[i],所以状态转移方程为:
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
其中 coins[j] 为第j个硬币,而 i - coins[j] 为钱数i减去其中一个硬币的值,剩余的钱数在 dp 数组中找到值,然后加1和当前 dp 数组中的值做比较,取较小的那个更新 dp 数组。先来看迭代的写法如下所示:
c++
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; ++i) {
for (int j = 0; j < coins.size(); ++j) {
if (coins[j] <= i) {
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return (dp[amount] > amount) ? -1 : dp[amount];
}
};
java
public class Solution {
public int coinChange(int[] coins, int amount) {
int max = amount + 1;
int[] dp = new int[amount + 1];
Arrays.fill(dp, max);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
python
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin in coins:
for x in range(coin, amount + 1):
dp[x] = min(dp[x], dp[x - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
递归加记忆数组
c++
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> memo(amount + 1, INT_MAX);
memo[0] = 0;
return coinChangeDFS(coins, amount, memo);
}
int coinChangeDFS(vector<int>& coins, int target, vector<int>& memo) {
if (target < 0) return - 1;
if (memo[target] != INT_MAX) return memo[target];
for (int i = 0; i < coins.size(); ++i) {
int tmp = coinChangeDFS(coins, target - coins[i], memo);
if (tmp >= 0) memo[target] = min(memo[target], tmp + 1);
}
return memo[target] = (memo[target] == INT_MAX) ? -1 : memo[target];
}
};
java
class Solution {
int[] memo;
public int coinChange(int[] coins, int amount) {
if(coins.length == 0){
return -1;
}
memo = new int[amount];
return findWay(coins,amount);
}
// memo[n] 表示钱币n可以被换取的最少的硬币数,不能换取就为-1
// findWay函数的目的是为了找到 amount数量的零钱可以兑换的最少硬币数量,返回其值int
public int findWay(int[] coins,int amount){
if(amount < 0){
return -1;
}
if(amount == 0){
return 0;
}
// 记忆化的处理,memo[n]用赋予了值,就不用继续下面的循环
// 直接的返回memo[n] 的最优值
if(memo[amount-1] != 0){
return memo[amount-1];
}
int min = Integer.MAX_VALUE;
for(int i = 0;i < coins.length;i++){
int res = findWay(coins,amount-coins[i]);
if(res >= 0 && res < min){
min = res + 1; // 加1,是为了加上得到res结果的那个步骤中,兑换的一个硬币
}
}
memo[amount-1] = (min == Integer.MAX_VALUE ? -1 : min);
return memo[amount-1];
}
}
python
def coinChange(coins: List[int], amount: int):
# 备忘录
memo = dict()
def coinChangeDFS(n):
# 查备忘录,避免重复计算
if n in memo: return memo[n]
if n == 0: return 0
if n < 0: return -1
res = float('INF')
for coin in coins:
subproblem = coinChangeDFS(n - coin)
if subproblem == -1: continue
res = min(res, 1 + subproblem)
# 记入备忘录
memo[n] = res if res != float('INF') else -1
return memo[n]
return coinChangeDFS(amount)
哈希
再来看一种使用 HashMap 来当记忆数组的递归解法:
c++
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
unordered_map<int, int> memo;
memo[0] = 0;
return coinChangeDFS(coins, amount, memo);
}
int coinChangeDFS(vector<int>& coins, int target, unordered_map<int, int>& memo) {
if (target < 0) return - 1;
if (memo.count(target)) return memo[target];
int cur = INT_MAX;
for (int i = 0; i < coins.size(); ++i) {
int tmp = coinChangeDFS(coins, target - coins[i], memo);
if (tmp >= 0) cur = min(cur, tmp + 1);
}
return memo[target] = (cur == INT_MAX) ? -1 : cur;
}
};
递归改进
首先是判断 start 是否小于0,因为需要从 coin 中取硬币,不能越界。
下面就是优化的核心了,看 target 是否能整除 coins[start],比如假如目标值是 15,如果当前取出了大小为5的硬币,这里做除法,可以立马知道只用大小为5的硬币就可以组成目标值 target,那么用 cur + target/coins[start] 来更新结果 res。
遍历 target/coins[start] 的次数,由于不能整除,只需要对余数调用递归函数,而且要把次数每次减1,并且再次求余数。
举个例子,比如 coins=[1,2,3],amount=11,那么 11 除以3,得3余2,那么i从3开始遍历。
那就是判断若 cur + i >= res - 1 成立,直接 break,不调用递归。
这里解释一下,cur + i 自不必说,是当前硬币个数 cur 加上新加的i个硬币,这里 cur+i 如果大于等于 res 的话,那么 res 是不会被更新的,那么为啥这里是大于等于 res-1 呢?
因为能运行到这一步,说明之前是无法整除的,那么余数一定存在,所以再次调用递归函数的 target 不为0,那么如果整除的话,cur 至少会加上1,所以又跟 res 相等了,还是不会使得 res 变得更小。
c++
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int res = INT_MAX, n = coins.size();
sort(coins.begin(), coins.end());
helper(coins, n - 1, amount, 0, res);
return (res == INT_MAX) ? -1 : res;
}
void helper(vector<int>& coins, int start, int target, int cur, int& res) {
if (start < 0) return;
if (target % coins[start] == 0) {
res = min(res, cur + target / coins[start]);
return;
}
for (int i = target / coins[start]; i >= 0; --i) {
if (cur + i >= res - 1) break;
helper(coins, start - 1, target - i * coins[start], cur + i, res);
}
}
};