LeetCode 322. 零钱兑换
问题描述
给定整数数组 coins
表示不同面额的硬币,以及整数 amount
表示总金额。计算凑成总金额所需的最少硬币个数。若无法凑出,返回 -1
。硬币数量无限。
示例:
输入:coins = [1, 2, 5], amount = 11
输出:3(5 + 5 + 1)
输入:coins = [2], amount = 3
输出:-1(无法凑出)
输入:coins = [1], amount = 0
输出:0(金额0无需硬币)
方法一:递归(记忆化搜索)
核心思路:
- 递归函数:
dfs(amount)
计算凑出amount
的最少硬币数。 - 终止条件:
amount == 0
:返回 0(无需硬币)。amount < 0
:返回 -1(无效)。
- 记忆化:用
memo
数组缓存结果,避免重复计算。 - 递归过程:
- 遍历每个硬币,递归计算
amount - coin
的最少硬币数。 - 若子问题有解,更新最小值;若所有子问题无解,返回 -1。
- 遍历每个硬币,递归计算
class Solution {
private int[] memo; // 记忆化数组
public int coinChange(int[] coins, int amount) {
memo = new int[amount + 1];
Arrays.fill(memo, -2); // 初始化为-2(未计算状态)
return dfs(coins, amount);
}
private int dfs(int[] coins, int amount) {
if (amount == 0)
return 0; // 金额为0,无需硬币
if (amount < 0)
return -1; // 金额为负,无效解
if (memo[amount] != -2)
return memo[amount]; // 已计算过,直接返回
int minCoins = Integer.MAX_VALUE;
for (int coin : coins) {
int subRes = dfs(coins, amount - coin); // 递归求解子问题
if (subRes >= 0) {
// 更新最小值(当前硬币+子问题硬币数)
minCoins = Math.min(minCoins, subRes + 1);
}
}
// 若minCoins未更新,说明无解;否则缓存结果
memo[amount] = (minCoins == Integer.MAX_VALUE) ? -1 : minCoins;
return memo[amount];
}
}
方法二:动态规划(DP数组)
核心思路:
- 状态定义:
dp[i]
表示凑出金额i
的最少硬币数。 - 初始化:
dp[0] = 0
(金额0无需硬币)。- 其他
dp[i]
初始化为amount + 1
(一个不可能达到的最大值)。
- 状态转移:
- 遍历金额
i
(1 到amount
)。 - 遍历每个硬币
coin
:- 若
i >= coin
,则dp[i] = min(dp[i], dp[i - coin] + 1)
。
- 若
- 遍历金额
- 结果:
- 若
dp[amount] > amount
,返回 -1(无解);否则返回dp[amount]
。
- 若
class Solution {
public int coinChange(int[] coins, int amount) {
if (amount == 0) return 0;
int[] dp = new int[amount + 1];
int max = amount + 1; // 初始化最大值
Arrays.fill(dp, max); // 初始化为最大值
dp[0] = 0; // 金额0的解为0
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (i >= coin) {
// 状态转移:尝试用当前硬币更新最少数量
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
// 若dp[amount]未被更新,说明无解
return dp[amount] > amount ? -1 : dp[amount];
}
}
方法三:动态规划(空间优化)
说明:
- 状态转移中
dp[i]
仅依赖dp[i - coin]
,且i
递增遍历,一维数组已是最优空间(O(amount)),无需进一步优化。
算法分析
- 时间复杂度:
- 递归:O(amount × n),n为硬币种类数。
- 动态规划:O(amount × n)。
- 空间复杂度:
- 递归:O(amount)(记忆化数组+递归栈)。
- 动态规划:O(amount)。
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1: 标准示例
int[] coins1 = {1, 2, 5};
System.out.println(solution.coinChange(coins1, 11)); // 3
// 测试用例2: 无解情况
int[] coins2 = {2};
System.out.println(solution.coinChange(coins2, 3)); // -1
// 测试用例3: 金额为0
int[] coins3 = {1};
System.out.println(solution.coinChange(coins3, 0)); // 0
// 测试用例4: 多硬币组合
int[] coins4 = {1, 3, 4};
System.out.println(solution.coinChange(coins4, 6)); // 2 (3+3)
}
关键点
- 递归记忆化:
- 避免重复计算,用
memo
缓存子问题结果。 - 初始化
memo
为特殊值(-2)区分未计算状态。
- 避免重复计算,用
- 动态规划:
- 初始化
dp[0] = 0
,其他为较大值(amount + 1
)。 - 内层循环遍历硬币,更新
dp[i]
。
- 初始化
- 边界处理:
- 金额为0时直接返回0。
- 无解时返回-1。
常见问题
- 为什么dp数组初始化为amount+1?
- 因为最坏情况是全部用1元硬币,硬币数量为amount。所以amount+1是一个不可能达到的更大值,用于后续比较。
- 递归方法中为什么用
-2
初始化?- 为了区分
未计算(-2)
和计算后无解(-1)
的情况。
- 为了区分
- 动态规划中为什么内层循环遍历硬币?
- 因为每个金额i,尝试所有可能的硬币,从而更新dp[i]。