LeetCode 322:零钱兑换:用最少硬币凑齐金额
2023-09-26 11:38:19
零钱兑换:动态规划算法的应用
前言
日常生活中,找零是不可避免的,对于商家而言,使用最少的硬币找零尤为重要。LeetCode 322 题“零钱兑换”正是这样一个问题,要求我们找出凑齐总金额所需的最小硬币数量。本文将深入剖析此题,探索其背后的动态规划算法。
问题分解
动态规划 是一种用于解决复杂问题的强大算法,通过将问题分解成较小的子问题,并利用子问题的解决方案逐层向上构建最终解。对于零钱兑换问题,我们可以定义一个状态 dp[i],表示凑齐金额 i 所需的最小硬币数量。
状态转移方程
状态转移方程 是动态规划的核心,它了状态之间的依赖关系。对于零钱兑换问题,有两种凑齐金额 i 的方法:
- 不使用最大面额硬币: 此时,我们需要使用 dp[i - coins[0]] 枚硬币,其中 coins[0] 是最大面额硬币。
- 使用最大面额硬币: 此时,我们需要使用 1 枚最大面额硬币和 dp[i - coins[0]] 枚硬币。
由此,我们得到状态转移方程:
dp[i] = min(dp[i - coins[0]] + 1, 1 + dp[i - coins[1]])
其中,coins[0] 和 coins[1] 分别为最大的两个硬币面额。
算法实现
基于状态转移方程,我们可以使用动态规划算法逐步求解问题:
- 初始化 dp 数组,其中 dp[0] = 0。
- 对于每个金额 i 从 1 到 amount:
- 如果 dp[i] 已经计算过,则跳过。
- 否则,对于每个硬币面额 coins[j],计算 dp[i - coins[j]] + 1 和 1 + dp[i - coins[j]],取最小值作为 dp[i]。
- 返回 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)。
结论
零钱兑换问题是一个经典的动态规划问题,通过分解问题并利用状态转移方程,我们可以高效地找到最优解。无论是对于商店经营者还是对算法爱好者而言,理解和掌握此算法都具有重要意义。
常见问题解答
-
为什么动态规划算法优于贪心算法?
动态规划算法考虑了所有可能的情况,而贪心算法只考虑当前最优解,可能导致非最优解。
-
算法的时间复杂度如何优化?
可以使用记忆化搜索技术减少重复计算,从而优化时间复杂度。
-
如何判断贪心算法是否适用于此问题?
贪心算法适用于当选择当前最优解时,总体最优解也会随之而来。零钱兑换问题并不满足此条件。
-
此算法还可以用于解决哪些问题?
动态规划算法广泛应用于背包问题、最长公共子序列问题和最小路径问题等各种问题。
-
是否有其他求解此问题的算法?
除了动态规划和贪心算法,还可以使用回溯和分支限界算法。