问题描述
小C有多种不同面值的硬币,每种硬币的数量是无限的。他希望知道,如何使用最少数量的硬币,凑出给定的总金额N。小C对硬币的组合方式很感兴趣,但他更希望在满足总金额的同时,使用的硬币数量尽可能少。
例如:小C有三种硬币,面值分别为 1, 2, 5。他需要凑出总金额为 18。一种最优的方案是使用三个 5 面值的硬币,一个 2 面值的硬币和一个 1 面值的硬币,总共五个硬币。
解题思路
本问题属于动态规划问题,目标是最小化硬币数量,同时构造出具体组合。
- 定义状态
- 使用数组
dp[i]表示凑出金额i所需的最少硬币数。 - 用辅助数组
coin_used[i]记录凑出金额i时最后使用的硬币。
- 使用数组
- 状态转移方程
- 对于每个硬币
coin:
- 对于每个硬币
其中,dp[x - coin] + 1 表示凑出金额 x 所需的硬币数量。
- 初始化
dp[0] = 0(凑出金额 0 不需要硬币)。dp[i] = ∞(其他金额初始为无穷大,表示暂时不可达)。
- 组合构造
- 根据
coin_used数组从金额amount开始反推,逐步找到构造方案。
- 根据
- 返回结果
- 如果
dp[amount]仍为无穷大,表示无法凑出金额,返回空数组。
- 如果
代码实现
def solution(coins, amount):
# 初始化 DP 数组
dp = [float('inf')] * (amount + 1)
dp[0] = 0 # 0 的金额需要 0 个硬币
# 用于追踪组合
coin_used = [-1] * (amount + 1)
# 动态规划计算最少硬币数量
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:
coin = coin_used[amount]
result.append(coin)
amount -= coin
return result
# 测试用例
if __name__ == "__main__":
print(solution([1, 2, 5], 18)) # 输出 [5, 5, 5, 2, 1]
print(solution([1, 3, 4], 6)) # 输出 [3, 3]
print(solution([5], 10)) # 输出 [5, 5]
测试样例
样例1:
输入:
coins = [1, 3, 4], amount = 6
输出:[3, 3]分析:
- 初始化:
dp = [0, inf, inf, ..., inf](长度为 19。coin_used = [-1, -1, -1, ..., -1](长度为 19)。
- 动态规划过程:
- 遍历硬币
1:
- 对于每个金额
x从1到18:
- 更新
dp[x] = dp[x - 1] + 1,即dp[x] = x(因为只用硬币1时,每个金额需要的硬币数等于金额大小)。- 记录硬币
coin_used[x] = 1。- 遍历硬币
2:
- 对于每个金额
x从2到18:
- 更新
dp[x] = min(dp[x], dp[x - 2] + 1):
- 如果用硬币
2凑出的硬币数更少,更新dp[x],并记录硬币coin_used[x] = 2。- 遍历硬币
5:
- 对于每个金额
x从5到18:
- 更新
dp[x] = min(dp[x], dp[x - 5] + 1):
- 如果用硬币
5凑出的硬币数更少,更新dp[x],并记录硬币coin_used[x] = 5。
- 结果构造:
- 从
amount = 18开始,根据coin_used逐步回溯:
- 使用硬币
5(18 → 13)。- 使用硬币
5(13 → 8)。- 使用硬币
5(8 → 3)。- 使用硬币
2(3 → 1)。- 使用硬币
1(1 → 0)。- 组合为
[5, 5, 5, 2, 1]。
样例2:
输入:
coins = [1, 3, 4], amount = 6
输出:[3, 3]分析:
- 初始化:
dp = [0, inf, inf, inf, inf, inf, inf](长度为 7)。coin_used = [-1, -1, -1, -1, -1, -1, -1]。
- 动态规划过程:
- 遍历硬币
1:
- 更新
dp[x]为x,记录coin_used[x] = 1。- 遍历硬币
3:
- 对于金额
3到6:
- 更新
dp[x] = min(dp[x], dp[x - 3] + 1):
- 使用硬币
3凑3和6时,更新为更优解,记录coin_used[x] = 3。- 遍历硬币
4:
- 对于金额
4到6:
dp[4]更新为1,dp[5]和dp[6]无法优化。
- 结果构造:
- 从
amount = 6开始,根据coin_used回溯:
- 使用硬币
3(6 → 3)。- 使用硬币
3(3 → 0)。- 组合为
[3, 3]。
样例3:
输入:
coins = [5], amount = 10
输出:[5, 5]分析:
- 初始化:
dp = [0, inf, inf, inf, ..., inf](长度为 11)。coin_used = [-1, -1, -1, ..., -1]。
- 动态规划过程:
- 遍历硬币
5:
- 对于金额
5到10:dp[5] = 1,coin_used[5] = 5。dp[10] = dp[5] + 1 = 2,coin_used[10] = 5。
- 结果构造:
- 从
amount = 10开始,根据coin_used回溯:
- 使用硬币
5(10 → 5)。- 使用硬币
5(5 → 0)。- 组合为
[5, 5]。
复杂度分析
-
时间复杂度
- 外层循环遍历硬币种类数
len(coins)。 - 内层循环遍历金额范围
amount。 - 因此时间复杂度为
O(len(coins)×amount)。
- 外层循环遍历硬币种类数
-
空间复杂度
- 动态数组
dp和coin_used需O(amount)空间。
- 动态数组