零钱兑换题解(记忆化搜索)
解题思路
本问题采用**记忆化搜索(Memoized DFS)**的方法解决,核心思想是将问题分解为子问题:对于每个硬币面额,选择使用或不使用该硬币,递归求解剩余金额的最小硬币数。通过缓存中间结果避免重复计算。
算法步骤
- 递归定义:定义
dfs(i, j)
表示用前i
种硬币(coins[0…i])凑出金额j
所需的最少硬币数。 - 终止条件:
- 当
i < 0
(无可用硬币)且j ≠ 0
时,返回无穷大(无解) - 当
j == 0
时,返回0(已凑出目标金额)
- 当
- 剪枝优化:若当前硬币面额大于剩余金额,只能跳过该硬币
- 状态转移:
- 不选当前硬币:
dfs(i-1, j)
- 选当前硬币:
dfs(i, j-coins[i]) + 1
(硬币数+1)
- 不选当前硬币:
- 记忆化存储:通过
@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表压缩为一维数组,利用滚动更新技术降低空间复杂度。通过逐个考虑每种硬币的影响,逐步更新各金额所需的最小硬币数。
算法步骤
- 初始化 DP 数组:
dp = [float('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)
- 对每个硬币
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
- 处理结果:
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 值 |
---|---|---|
1 | min(inf, dp[0]+1=1) → 1 | 1 |
2 | min(inf, dp[1]+1=2) → 2 | 2 |
3 | min(inf, dp[2]+1=3) → 3 | 3 |
… | … | … |
11 | min(inf, dp[10]+1=11) → 11 | 11 |
更新后 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
金额 | 原值 | 计算过程 | 新值 |
---|---|---|---|
2 | 2 | min(2, dp[0]+1=1) → 1 | 1 |
3 | 3 | min(3, dp[1]+1=2) → 2 | 2 |
4 | 4 | min(4, dp[2]+1=2) → 2 | 2 |
5 | 5 | min(5, dp[3]+1=3) → 3 | 3 |
6 | 6 | min(6, dp[4]+1=3) → 3 | 3 |
… | … | … | … |
11 | 11 | min(11, dp[9]+1=6) → 6 | 6 |
更新后 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
金额 | 原值 | 计算过程 | 新值 |
---|---|---|---|
5 | 3 | min(3, dp[0]+1=1) → 1 | 1 |
6 | 3 | min(3, dp[1]+1=2) → 2 | 2 |
7 | 4 | min(4, dp[2]+1=2) → 2 | 2 |
8 | 4 | min(4, dp[3]+1=3) → 3 | 3 |
9 | 5 | min(5, dp[4]+1=3) → 3 | 3 |
10 | 5 | min(5, dp[5]+1=2) → 2 | 2 |
11 | 6 | min(6, dp[6]+1=3) → 3 | 3 |
最终 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