最优硬币组合问题
问题描述
在日常生活中,我们经常会遇到需要找零的情况。小C有多种不同面值的硬币,并且每种硬币的数量是无限的。他希望知道,如何使用最少数量的硬币来凑出给定的总金额 ( N )。在这个过程中,小C对硬币的组合方式非常感兴趣,但他更希望在满足总金额的同时,使用的硬币数量尽可能少。
例如,小C有三种硬币,面值分别为 1, 2, 5。他需要凑出总金额为 18。一种最优的方案是使用三个 5 面值的硬币,一个 2 面值的硬币和一个 1 面值的硬币,总共五个硬币。
思路解析
要解决这个问题,我们可以使用动态规划(Dynamic Programming)的方法。动态规划是一种通过将复杂问题分解为更简单的子问题来解决问题的技术。对于最优硬币组合问题,我们可以定义一个状态数组 dp
,其中 dp[i]
表示凑出金额 ( i ) 所需的最小硬币数量。同时,我们还需要一个数组 coin_used
来记录凑出每个金额时所用的最后一枚硬币。
步骤
-
初始化:
- 创建一个
dp
数组,大小为 ( N + 1 ),初始值为无穷大(表示无法凑出该金额),其中dp[0]
初始化为 0(凑出 0 元不需要任何硬币)。 - 创建一个
coin_used
数组,用于记录每个金额所用的最后一枚硬币。
- 创建一个
-
状态转移:
- 对于每种硬币,遍历所有可能的金额 ( j ) 从硬币面值到目标金额 ( N ):
- 如果使用当前硬币可以减少所需的硬币数量,则更新
dp[j]
和coin_used[j]
。
- 如果使用当前硬币可以减少所需的硬币数量,则更新
- 对于每种硬币,遍历所有可能的金额 ( j ) 从硬币面值到目标金额 ( N ):
-
构造结果:
- 从目标金额 ( N ) 开始,通过
coin_used
数组回溯,构造出具体的硬币组合。
- 从目标金额 ( N ) 开始,通过
代码详解
以下是实现最优硬币组合问题的代码:
def solution(array, total):
# 初始化 dp 数组和 coin_used 数组
dp = [float('inf')] * (total + 1)
coin_used = [-1] * (total + 1)
# 0 元需要 0 个硬币
dp[0] = 0
# 动态规划填充 dp 数组
for coin in array:
for j in range(coin, total + 1):
if dp[j - coin] + 1 < dp[j]:
dp[j] = dp[j - coin] + 1
coin_used[j] = coin
# 如果 dp[total] 仍为无穷大,表示无法凑出该金额
if dp[total] == float('inf'):
return []
# 构造结果
result = []
while total > 0:
result.append(coin_used[total])
total -= coin_used[total]
return result
if __name__ == "__main__":
# 测试用例
print(solution([1, 2, 5], 18) == [5, 5, 5, 2, 1]) # 输出: True
print(solution([1, 3, 4], 6) == [3, 3]) # 输出: True
print(solution([5], 10) == [5, 5]) # 输出: True
print(solution([2, 5], 3) == []) # 输出: True
代码分析
-
初始化:
dp
数组的大小为total + 1
,初始值为无穷大,表示无法凑出该金额,除了dp[0]
为 0,表示凑出 0 元不需要任何硬币。
-
动态规划:
- 对于每种硬币,更新
dp
和coin_used
数组,确保我们能找到最优解。对于每个金额 ( j ),如果使用当前硬币可以减少所需的硬币数量,则更新dp[j]
和coin_used[j]
。
- 对于每种硬币,更新
-
结果构造:
- 通过回溯
coin_used
数组,构造出具体的硬币组合,直到金额减少到 0。
- 通过回溯
复杂度分析
- 时间复杂度:O(n×m),其中 ( n ) 是硬币种类数,( m ) 是目标金额。因为我们需要遍历每种硬币和每个金额。
- 空间复杂度:O(m) ,用于存储
dp
和coin_used
数组。
总结
最优硬币组合问题是一个经典的动态规划问题,通过合理的状态定义和状态转移方程,我们能够高效地解决这一问题。通过对问题的深入理解,我们可以将复杂的组合问题转化为简单的子问题,从而减少计算量,提高算法的效率。
在实际应用中,类似的动态规划技术可以广泛应用于其他问题,例如背包问题、最短路径问题等。掌握动态规划的基本思想和技巧,对于算法学习和编程能力的提升具有重要意义。
通过这个问题的解决,我们不仅学会了如何实现动态规划,还体会到了思维的转变,从而能够更好地应对复杂的算法问题。这种方法论的掌握,将为我们在今后的学习和工作中提供强有力的支持。