动态规划 零钱兑换

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)
}

关键点

  1. 递归记忆化
    • 避免重复计算,用 memo 缓存子问题结果。
    • 初始化 memo 为特殊值(-2)区分未计算状态。
  2. 动态规划
    • 初始化 dp[0] = 0,其他为较大值(amount + 1)。
    • 内层循环遍历硬币,更新 dp[i]
  3. 边界处理
    • 金额为0时直接返回0。
    • 无解时返回-1。

常见问题

  1. 为什么dp数组初始化为amount+1?
    • 因为最坏情况是全部用1元硬币,硬币数量为amount。所以amount+1是一个不可能达到的更大值,用于后续比较。
  2. 递归方法中为什么用-2初始化?
    • 为了区分未计算(-2)和计算后无解(-1)的情况。
  3. 动态规划中为什么内层循环遍历硬币?
    • 因为每个金额i,尝试所有可能的硬币,从而更新dp[i]。
### C++ 动态规划 实现 零钱兑换 算法 示例 代码 为了实现零钱兑换问题,采用动态规划方法是一种有效的方式。通过构建一个大小为 `amount + 1` 的数组 `dp` 来存储每一个子问题的结果,其中 `dp[i]` 表示凑齐金额 `i` 所需的最小硬币数量。 初始化时设置 `dp[0] = 0`,因为要凑成金额 0 不需要任何硬币。对于其他位置,则先设为无穷大(这里可以用一个非常大的数值代替),表示尚未找到可行方案。 接着遍历所有可能使用的硬币种类,在每次迭代过程中更新当前考虑的最大金额范围内的各个目标值对应的最少硬币数目: ```cpp class Solution { public: int coinChange(vector<int>& coins, int amount) { vector<int> dp(amount + 1, INT_MAX); dp[0] = 0; for (auto& coin : coins) { // 遍历物品(硬币) for (int j = coin; j <= amount; ++j) { // 更新可达的目标金额 if (dp[j - coin] != INT_MAX && dp[j - coin] + 1 < dp[j]) { dp[j] = dp[j - coin] + 1; } } } return dp[amount] == INT_MAX ? -1 : dp[amount]; } }; ``` 上述代码实现了基于动态规划思想来解决问题的核心逻辑[^2]。此版本不仅能够处理基本案例,还适用于更广泛的输入情况,包括但不限于不同的硬币集合和较大的总额度请求。 #### 关键点解释 - **状态定义**:`dp[i]` 定义为构成金额 `i` 所需的最少硬币数。 - **边界条件**:当所需金额为 0 (`dp[0]`) 时不消耗任何硬币。 - **状态转移方程**:对于每个新加入的硬币面额 `coin` 和其后的每一笔交易金额 `j >= coin` ,尝试用这枚新的硬币去减少之前已经计算好的较小金额下的最佳解 `dp[j - coin]` 加上这一次额外增加的一枚硬币(`+1`)作为候选答案,并取两者之间的较小者作为最终结果保存回原表项中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值