做题笔记:最少硬币问题
题目分析
-
问题描述:
- 给定一个正整数总金额 NN 和多种面值的硬币,硬币的数量无限。
- 要求使用最少数量的硬币组合来凑出总金额 NN。
- 如果无法凑出总金额,则返回空列表。
-
例子:
- 硬币面值:1,2,51, 2, 5
- 目标金额:18
- 最优解:5,5,5,2,15, 5, 5, 2, 1,即总共使用 5 枚硬币。
-
问题性质:
- 最优化问题:在所有可能的组合中找到硬币数最少的一种。
- 无限背包问题:每种硬币可以使用无限次。
- 动态规划特性:问题可以分解为子问题,通过记录中间结果避免重复计算。
解题思路
-
状态定义:
- 设 dp[i]dp[i] 表示凑出金额 ii 所需的最少硬币数量。
- 辅助数组
coin_used记录凑出金额 ii 时使用的最后一枚硬币,便于回溯组合方案。
-
转移方程:
-
假设当前硬币面值为 cc,金额为 xx,转移方程为: dp[x]=min(dp[x],dp[x−c]+1)dp[x] = \min(dp[x], dp[x - c] + 1)
- 如果使用当前硬币 cc,从金额 x−cx - c 转移到 xx。
- dp[x−c]+1dp[x - c] + 1 表示之前的最优解加上当前硬币的数量。
-
-
边界条件:
- dp[0]=0dp[0] = 0:金额为 0 时,不需要硬币。
- dp[i]=∞dp[i] = \infty(初始值):表示金额 ii 暂时无法凑出。
-
回溯路径:
- 根据
coin_used数组,从目标金额开始回溯,依次记录使用的硬币,直到金额为 0。
- 根据
-
复杂度分析:
- 时间复杂度:O(amount×len(coins))O(\text{amount} \times \text{len(coins)}),因为外层遍历硬币,内层遍历金额。
- 空间复杂度:O(amount)O(\text{amount}),需要额外的 dpdp 和
coin_used数组。
代码解析
def solution(coins, amount):
# 初始化 dp 数组和 coin_used 数组
dp = [float('inf')] * (amount + 1)
dp[0] = 0
coin_used = [-1] * (amount + 1)
# 动态规划更新 dp 数组
for coin in coins:
for x in range(coin, amount + 1):
if dp[x - coin] + 1 < dp[x]:
dp[x] = dp[x - coin] + 1
coin_used[x] = coin
# 如果无法凑出目标金额,返回空列表
if dp[amount] == float('inf'):
return []
# 回溯硬币组合
result = []
while amount > 0:
result.append(coin_used[amount])
amount -= coin_used[amount]
return result
核心部分分析:
-
初始化:
- dpdp 数组初始化为无穷大,表示初始状态下无法凑出任何金额。
- dp[0]=0dp[0] = 0:凑出金额 0 不需要硬币。
-
动态规划递推:
- 遍历每种硬币,对应金额从 coincoin 到 amountamount 更新 dpdp 数组。
- 每次更新时,记录当前选择的硬币到
coin_used数组。
-
结果判定:
- 如果 dp[amount]dp[amount] 仍为无穷大,表示目标金额无法凑出,返回空列表。
-
回溯硬币组合:
- 从
coin_used数组中逐步找出使用的硬币,直到金额为 0。
- 从
总结
-
优点:
- 动态规划方法避免了重复计算,效率高。
- 回溯路径可以清楚地还原最优硬币组合。
-
适用范围:
- 此方法适用于任意硬币面值组合,且硬币数量无限的问题。
-
注意事项:
- 如果硬币面值不能凑出目标金额,需要特别处理返回空列表。
- 硬币面值可能不是有序的,代码逻辑中无需排序。
-
扩展问题:
- 如果硬币数量有限,该问题可以用 完全背包 方法扩展解决。