【LeetCode Hot100 刷题日记 (85/100)】322. 零钱兑换 —— 动态规划、完全背包、数组 💰

5 阅读5分钟

📌 题目链接:322. 零钱兑换 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:动态规划、完全背包、数组

⏱️ 目标时间复杂度:O(amount × n) (n 为硬币种类数)

💾 空间复杂度:O(amount)


🔍 题目分析

给定一个整数数组 coins,表示不同面额的硬币;以及一个整数 amount,表示总金额。
要求:计算并返回可以凑成总金额所需的最少硬币个数。若无法凑出,则返回 -1
每种硬币数量 无限(典型「完全背包」问题特征)。

✅ 示例回顾

  • 示例 1coins = [1, 2, 5], amount = 11 → 输出 3(5+5+1)
  • 示例 2coins = [2], amount = 3 → 输出 -1
  • 示例 3coins = [1], amount = 0 → 输出 0

💡 注意边界:amount = 0 时,不需要任何硬币,答案是 0


🧠 核心算法及代码讲解

本题是 「完全背包问题」 的经典变种 —— 不求最大价值,而是求 最小物品数量 来恰好装满背包。

🎯 为什么是「完全背包」?

  • 物品:硬币(每种可选无限次)
  • 背包容量:目标金额 amount
  • 目标:用最少数量的物品(硬币)填满背包(凑出 amount

⚠️ 区别于 0-1 背包(每件物品只能用一次),完全背包允许重复选取。


📌 动态规划状态定义

dp[i] 表示 凑出金额 i 所需的最少硬币数

✅ 初始状态:

  • dp[0] = 0:凑出金额 0 不需要任何硬币。
  • 其他 dp[i] 初始化为一个“不可能的大值”(如 amount + 1),便于后续取 min

🔁 状态转移方程:

对每个金额 i(从 1 到 amount),遍历所有硬币 coin

if (coin <= i) {
    dp[i] = min(dp[i], dp[i - coin] + 1);
}

🧩 含义:如果当前硬币 coin 能用(coin <= i),那么凑出 i 的方案可以由 i - coin 的最优解 + 1 枚硬币得到。

🚫 无解判断:

若最终 dp[amount] > amount,说明无法凑出,返回 -1
(因为最多用 amount 枚 1 元硬币就能凑出 amount,若结果比这还大,必无解)


💻 核心算法代码(带详细行注释)

// dp[i] 表示凑出金额 i 所需的最少硬币数
vector<int> dp(amount + 1, Max);  // 初始化为一个大于可能答案的值
dp[0] = 0;                        // 基础情况:金额 0 需要 0 枚硬币

for (int i = 1; i <= amount; ++i) {           // 遍历所有金额 1 ~ amount
    for (int j = 0; j < (int)coins.size(); ++j) {  // 遍历每种硬币
        if (coins[j] <= i) {                  // 当前硬币面额不超过当前金额
            dp[i] = min(dp[i], dp[i - coins[j]] + 1);  // 状态转移:取最小值
        }
    }
}

// 若 dp[amount] 仍为初始大值,说明无法凑出
return dp[amount] > amount ? -1 : dp[amount];

✅ 此写法为 「自底向上」的动态规划(Bottom-up DP) ,避免了递归栈开销,效率更高。


🧩 解题思路(分步拆解)

  1. 明确问题类型:无限硬币 → 完全背包 → 动态规划。

  2. 定义状态dp[i] = 凑出金额 i 的最少硬币数

  3. 初始化

    • dp[0] = 0
    • 其余设为 amount + 1(比最大可能值 amount 还大,确保 min 能更新)
  4. 状态转移

    • 对每个金额 i,尝试所有硬币 coin
    • coin <= i,则 dp[i] = min(dp[i], dp[i - coin] + 1)
  5. 结果判断

    • dp[amount] > amount → 无解,返回 -1
    • 否则返回 dp[amount]

🌟 类比爬楼梯:把 amount 看作台阶,coins 是每次能跨的步数,求最少步数登顶!


📊 算法分析

项目分析
时间复杂度O(amount × n),其中 n = coins.size()。需遍历 amount 个状态,每个状态遍历 n 种硬币。
空间复杂度O(amount),仅需一维 dp 数组。
是否可优化空间?已是最优!一维 DP 无法再压缩。
面试高频点✅ 完全背包 vs 0-1 背包区别 ✅ 状态定义与转移逻辑 ✅ 边界处理(amount=0) ✅ 无解判断技巧

💬 面试官可能会问

  • 如果硬币数量有限怎么办?→ 变成多重背包,需加计数维度。
  • 能否输出具体用了哪些硬币?→ 需额外记录路径(parent 数组)。
  • 能否用 BFS 解?→ 可以!将金额看作图节点,硬币为边,求最短路径(但空间可能更大)。

💻 完整代码

C++ 版本

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int Max = amount + 1;
        vector<int> dp(amount + 1, Max);
        dp[0] = 0;
        for (int i = 1; i <= amount; ++i) {
            for (int j = 0; j < (int)coins.size(); ++j) {
                if (coins[j] <= i) {
                    dp[i] = min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    Solution sol;
    // 测试用例 1
    vector<int> coins1 = {1, 2, 5};
    cout << sol.coinChange(coins1, 11) << "\n"; // 输出: 3

    // 测试用例 2
    vector<int> coins2 = {2};
    cout << sol.coinChange(coins2, 3) << "\n"; // 输出: -1

    // 测试用例 3
    vector<int> coins3 = {1};
    cout << sol.coinChange(coins3, 0) << "\n"; // 输出: 0

    return 0;
}

JavaScript 版本

/**
 * @param {number[]} coins
 * @param {number} amount
 * @return {number}
 */
var coinChange = function(coins, amount) {
    const MAX = amount + 1;
    const dp = new Array(amount + 1).fill(MAX);
    dp[0] = 0;

    for (let i = 1; i <= amount; i++) {
        for (const coin of coins) {
            if (coin <= i) {
                dp[i] = Math.min(dp[i], dp[i - coin] + 1);
            }
        }
    }

    return dp[amount] > amount ? -1 : dp[amount];
};

// 测试
console.log(coinChange([1, 2, 5], 11)); // 3
console.log(coinChange([2], 3));        // -1
console.log(coinChange([1], 0));        // 0

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!