中等

零钱兑换

动态规划完全背包LeetCode 322
相关算法文章:

题目描述

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

示例

示例 1:

输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1

示例 2:

输入: coins = [2], amount = 3
输出: -1
解释: 用面额 2 无法凑出 3

示例 3:

输入: coins = [1], amount = 0
输出: 0

示例 4:

输入: coins = [1, 5, 10], amount = 18
输出: 5
解释: 18 = 10 + 5 + 1 + 1 + 1

解题思路

这是一个经典的完全背包问题。每种硬币可以使用无限次,目标是凑出金额 amount 的最少硬币数。

状态定义dp[j] = 凑出金额 j 所需的最少硬币数

状态转移方程dp[j] = min(dp[j], dp[j - coin] + 1)

初始化dp[0] = 0,其余 dp[j] = Infinity(表示无法凑出)

遍历顺序:硬币外层、金额内层,金额正序遍历(保证每种硬币可以重复使用)。

最后,若 dp[amount] 仍为 Infinity,返回 -1

代码实现

/**
 * 零钱兑换 — 基础版(金额外层、硬币内层)
 * @param {number[]} coins - 硬币面额数组
 * @param {number} amount - 目标金额
 * @returns {number} 最少硬币数,无法凑出返回 -1
 */
function coinChange(coins, amount) {
    if (!coins.length) return -1;
    if (!amount) return 0;

    const dp = new Array(amount + 1).fill(Number.MAX_SAFE_INTEGER);
    dp[0] = 0;

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

/**
 * 零钱兑换 — 优化版:硬币外层、金额内层(标准完全背包写法)
 * @param {number[]} coins - 硬币面额数组
 * @param {number} amount - 目标金额
 * @returns {number} 最少硬币数,无法凑出返回 -1
 */
function coinChangeOptimized(coins, amount) {
    if (!coins.length) return -1;
    if (!amount) return 0;

    coins = [...coins].sort((a, b) => a - b);

    const dp = new Array(amount + 1).fill(Infinity);
    dp[0] = 0;

    for (const coin of coins) {
        for (let j = coin; j <= amount; j++) {
            dp[j] = Math.min(dp[j], dp[j - coin] + 1);
        }
    }
    return dp[amount] === Infinity ? -1 : dp[amount];
}

复杂度分析

  • 时间复杂度:O(amount × coins.length)。
  • 空间复杂度:O(amount),dp 数组大小。

示例输入 / 输出

下面给出一组输入输出示例,便于对照题意与结果。

编号 coin-change难度 中等

输入

coins = [1, 2, 5], amount = 11

输出

3