Leetcode-100 零钱兑换

零钱兑换题解(记忆化搜索)

解题思路

本问题采用**记忆化搜索(Memoized DFS)**的方法解决,核心思想是将问题分解为子问题:对于每个硬币面额,选择使用或不使用该硬币,递归求解剩余金额的最小硬币数。通过缓存中间结果避免重复计算。

算法步骤

  1. 递归定义:定义dfs(i, j)表示用前i种硬币(coins[0…i])凑出金额j所需的最少硬币数。
  2. 终止条件
    • i < 0(无可用硬币)且j ≠ 0时,返回无穷大(无解)
    • j == 0时,返回0(已凑出目标金额)
  3. 剪枝优化:若当前硬币面额大于剩余金额,只能跳过该硬币
  4. 状态转移
    • 不选当前硬币dfs(i-1, j)
    • 选当前硬币dfs(i, j-coins[i]) + 1(硬币数+1)
  5. 记忆化存储:通过@cache缓存所有(i,j)的结果

具体例子分析

coins = [1,2,5]amount = 11为例:

硬币索引金额选择方式结果
2(5元)11选5元 → 剩余6元dfs(2,6)+1
最终路径为选2个5元+1个1元,共3枚硬币。

递归调用树的关键路径:
dfs(2,11) → min(dfs(1,11), dfs(2,6)+1)
dfs(2,6) → min(dfs(1,6), dfs(2,1)+1)
dfs(2,1) → min(dfs(1,1), dfs(2,-4)+1) → 选1元硬币

代码实现

from typing import List
from functools import cache

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        inf = float('inf')
        
        @cache
        def dfs(i, j):
            # 终止条件:无硬币可用时
            if i < 0:
                return 0 if j == 0 else inf
            
            # 已凑出目标金额
            if j == 0:
                return 0
            
            # 当前硬币面值大于剩余金额,只能跳过
            if j < coins[i]:
                return dfs(i-1, j)
            
            # 选择:不选当前硬币 vs 选当前硬币
            return min(dfs(i-1, j), dfs(i, j - coins[i]) + 1)
        
        ans = dfs(len(coins)-1, amount)
        return ans if ans != inf else -1

复杂度分析

  • 时间复杂度: O ( n × amount ) O(n \times \text{amount}) O(n×amount)

    • 其中 n n n 为硬币种数, amount \text{amount} amount 为目标金额。
    • 每个状态 ( i , j ) (i, j) (i,j) 最多计算一次,总状态数为 n × amount n \times \text{amount} n×amount
    • 因此,总体时间复杂度为 O ( n × amount ) O(n \times \text{amount}) O(n×amount)
  • 空间复杂度: O ( n × amount ) O(n \times \text{amount}) O(n×amount)

    • 由于 记忆化缓存 需要存储所有可能的状态,因此空间复杂度为 O ( n × amount ) O(n \times \text{amount}) O(n×amount)

优化

核心思想
将二维DP表压缩为一维数组,利用滚动更新技术降低空间复杂度。通过逐个考虑每种硬币的影响,逐步更新各金额所需的最小硬币数。

算法步骤

  1. 初始化 DP 数组
dp = [float('inf')] * (amount + 1)
dp[0] = 0  # 金额 0 需要 0 个硬币
  1. 遍历每种硬币
for coin in coins:
    for j in range(coin, amount + 1):
        dp[j] = min(dp[j], dp[j - coin] + 1)
  • 对每个硬币 coin,更新所有 ≥ coin 的金额 j
  • 状态转移方程
    b e g i n : m a t h : d i s p l a y begin:math:display begin:math:display
    dp[j] = \min(\text{不选该硬币时的解}, \text{选该硬币后的解} + 1)
    e n d : m a t h : d i s p l a y end:math:display end:math:display
  1. 处理结果
return dp[amount] if dp[amount] != float('inf') else -1
  • dp[amount] == inf,说明无法凑成该金额,返回 -1

时间复杂度与空间复杂度

  • 时间复杂度: O ( n × amount ) O(n \times \text{amount}) O(n×amount)

    • 需遍历 n n n 种硬币,每种硬币遍历 amount 金额。
  • 空间复杂度: O ( amount ) O(\text{amount}) O(amount)

    • 仅需 一维数组 存储各金额的最小硬币数。

初始状态

硬币列表coins = [1, 2, 5]
目标金额amount = 11
初始化DP数组

dp = [0, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf]  
# 索引:0  1   2   3   4   5   6   7   8   9   10  11

处理每个硬币的详细过程


第一步:处理 1 元硬币
  • 更新规则dp[j] = min(dp[j], dp[j-1] + 1)
  • 更新范围j = 1 → 11
金额计算过程更新后 dp
1min(inf, dp[0]+1=1) → 11
2min(inf, dp[1]+1=2) → 22
3min(inf, dp[2]+1=3) → 33
11min(inf, dp[10]+1=11) → 1111

更新后 DP 数组

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

第二步:处理 2 元硬币
  • 更新规则dp[j] = min(dp[j], dp[j-2] + 1)
  • 更新范围j = 2 → 11
金额原值计算过程新值
22min(2, dp[0]+1=1) → 11
33min(3, dp[1]+1=2) → 22
44min(4, dp[2]+1=2) → 22
55min(5, dp[3]+1=3) → 33
66min(6, dp[4]+1=3) → 33
1111min(11, dp[9]+1=6) → 66

更新后 DP 数组

[0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6]

第三步:处理 5 元硬币
  • 更新规则dp[j] = min(dp[j], dp[j-5] + 1)
  • 更新范围j = 5 → 11
金额原值计算过程新值
53min(3, dp[0]+1=1) → 11
63min(3, dp[1]+1=2) → 22
74min(4, dp[2]+1=2) → 22
84min(4, dp[3]+1=3) → 33
95min(5, dp[4]+1=3) → 33
105min(5, dp[5]+1=2) → 22
116min(6, dp[6]+1=3) → 33

最终 DP 数组

[0, 1, 1, 2, 2, 1, 2, 2, 3, 3, 2, 3]

结果解析

  • 金额 11 的最优解dp[11] = 3
  • 最优组合5元 × 2 + 1元 × 1 = 11 元
  • 递推逻辑验证
dp[11] = dp[11-5] + 1 = dp[6] + 1 = 2 + 1 = 3  
dp[6]  = dp[6-5] + 1 = dp[1] + 1 = 1 + 1 = 2

代码实现

from typing import List
import math

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        inf = math.inf
        dp = [inf] * (amount + 1)
        dp[0] = 0  # 初始条件:金额0需要0个硬币
        
        for coin in coins:
            for j in range(coin, amount + 1):
                dp[j] = min(dp[j], dp[j - coin] + 1)
        
        return dp[amount] if dp[amount] != inf else -1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值