返回

LeetCode 322:零钱兑换:用最少硬币凑齐金额

见解分享

零钱兑换:动态规划算法的应用

前言

日常生活中,找零是不可避免的,对于商家而言,使用最少的硬币找零尤为重要。LeetCode 322 题“零钱兑换”正是这样一个问题,要求我们找出凑齐总金额所需的最小硬币数量。本文将深入剖析此题,探索其背后的动态规划算法。

问题分解

动态规划 是一种用于解决复杂问题的强大算法,通过将问题分解成较小的子问题,并利用子问题的解决方案逐层向上构建最终解。对于零钱兑换问题,我们可以定义一个状态 dp[i],表示凑齐金额 i 所需的最小硬币数量。

状态转移方程

状态转移方程 是动态规划的核心,它了状态之间的依赖关系。对于零钱兑换问题,有两种凑齐金额 i 的方法:

  1. 不使用最大面额硬币: 此时,我们需要使用 dp[i - coins[0]] 枚硬币,其中 coins[0] 是最大面额硬币。
  2. 使用最大面额硬币: 此时,我们需要使用 1 枚最大面额硬币和 dp[i - coins[0]] 枚硬币。

由此,我们得到状态转移方程:

dp[i] = min(dp[i - coins[0]] + 1, 1 + dp[i - coins[1]])

其中,coins[0] 和 coins[1] 分别为最大的两个硬币面额。

算法实现

基于状态转移方程,我们可以使用动态规划算法逐步求解问题:

  1. 初始化 dp 数组,其中 dp[0] = 0。
  2. 对于每个金额 i 从 1 到 amount:
    • 如果 dp[i] 已经计算过,则跳过。
    • 否则,对于每个硬币面额 coins[j],计算 dp[i - coins[j]] + 1 和 1 + dp[i - coins[j]],取最小值作为 dp[i]。
  3. 返回 dp[amount]。
def coinChange(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0

    for i in range(1, amount + 1):
        for coin in coins:
            if i - coin >= 0:
                dp[i] = min(dp[i], dp[i - coin] + 1)

    return dp[amount] if dp[amount] != float('inf') else -1

复杂度分析

此算法的时间复杂度为 O(amount * coins),其中 amount 为总金额,coins 为硬币面额数组的长度。空间复杂度为 O(amount)。

扩展:贪心算法与动态规划

除了动态规划,还可以使用贪心算法 解决此问题。贪心算法每次都选择面额最大的硬币。然而,贪心算法并不总是能得到最优解。例如,对于硬币面额 [1, 2, 5] 和金额 11,动态规划算法得到 3 枚硬币(5、5、1),而贪心算法得到 4 枚硬币(5、2、2、2)。

结论

零钱兑换问题是一个经典的动态规划问题,通过分解问题并利用状态转移方程,我们可以高效地找到最优解。无论是对于商店经营者还是对算法爱好者而言,理解和掌握此算法都具有重要意义。

常见问题解答

  1. 为什么动态规划算法优于贪心算法?

    动态规划算法考虑了所有可能的情况,而贪心算法只考虑当前最优解,可能导致非最优解。

  2. 算法的时间复杂度如何优化?

    可以使用记忆化搜索技术减少重复计算,从而优化时间复杂度。

  3. 如何判断贪心算法是否适用于此问题?

    贪心算法适用于当选择当前最优解时,总体最优解也会随之而来。零钱兑换问题并不满足此条件。

  4. 此算法还可以用于解决哪些问题?

    动态规划算法广泛应用于背包问题、最长公共子序列问题和最小路径问题等各种问题。

  5. 是否有其他求解此问题的算法?

    除了动态规划和贪心算法,还可以使用回溯和分支限界算法。