最优硬币组合问题 | 豆包MarsCode AI刷题

126 阅读4分钟

问题描述 小C有多种不同面值的硬币,每种硬币的数量是无限的。他希望知道,如何使用最少数量的硬币,凑出给定的总金额N。小C对硬币的组合方式很感兴趣,但他更希望在满足总金额的同时,使用的硬币数量尽可能少。 例如:小C有三种硬币,面值分别为 1, 2, 5。他需要凑出总金额为 18。一种最优的方案是使用三个 5 面值的硬币,一个 2 面值的硬币和一个 1 面值的硬币,总共五个硬币。 测试样例 样例1:

输入:coins = [1, 2, 5], amount = 18 输出:[5, 5, 5, 2, 1]

样例2:

输入:coins = [1, 3, 4], amount = 6 输出:[3, 3]

样例3:

输入:coins = [5], amount = 10 输出:[5, 5]

问题分析:

给定不同面值的硬币,面值数组 coins 和目标金额 amount。我们需要通过最少数量的硬币组合来凑出 amount

解题思路:

  1. 动态规划(DP) :

    • 定义一个数组 dp,其中 dp[i] 表示凑出金额 i 所需的最小硬币数。
    • 初始时,dp[0] = 0,表示凑出金额为0时不需要任何硬币。
    • 对于其他的金额 i,我们初始化 dp[i] 为一个较大的值,表示暂时无法凑出该金额。
    • 对每个硬币面值 coin,我们遍历所有金额 i(从 coin 到 amount),更新 dp[i] 的值为 dp[i - coin] + 1,表示使用当前硬币后凑出金额 i 的最小硬币数。
  2. 找出硬币组合:

    • 在计算出 dp[amount] 的最小硬币数后,我们还需要回溯硬币组合,找出具体使用哪些硬币来构成目标金额。

具体步骤:

  1. 初始化一个大小为 amount + 1 的 dp 数组,初值为一个较大的数(如 float('inf')),表示一开始不能凑出任何金额。设置 dp[0] = 0,因为凑出金额为 0 不需要硬币。
  2. 遍历每个硬币,更新 dp 数组,记录出使用最少硬币组合出各个金额。
  3. 最后,如果 dp[amount] 仍然是无穷大,说明无法凑出该金额,否则就可以从 dp 数组中找到最少的硬币数。
  4. 然后,从 dp 数组回溯出具体的硬币组合。

代码实现:

python
def coinChange(coins, amount):
    # Step 1: DP initialization
    dp = [float('inf')] * (amount + 1)  # dp[i] represents the minimum coins for amount i
    dp[0] = 0  # No coins needed to make amount 0
    
    # Step 2: Fill dp array using the coin values
    for coin in coins:
        for i in range(coin, amount + 1):
            dp[i] = min(dp[i], dp[i - coin] + 1)
    
    # Step 3: If dp[amount] is still infinity, it means no solution
    if dp[amount] == float('inf'):
        return []  # No solution
    
    # Step 4: Backtrack to find the exact coins used
    result = []
    current_amount = amount
    while current_amount > 0:
        for coin in coins:
            if current_amount >= coin and dp[current_amount] == dp[current_amount - coin] + 1:
                result.append(coin)
                current_amount -= coin
                break
    
    return result

# 示例:
coins1 = [1, 2, 5]
amount1 = 18
print(coinChange(coins1, amount1))  # 输出:[5, 5, 5, 2, 1]

coins2 = [1, 3, 4]
amount2 = 6
print(coinChange(coins2, amount2))  # 输出:[3, 3]

coins3 = [5]
amount3 = 10
print(coinChange(coins3, amount3))  # 输出:[5, 5]

代码解释:

  1. 动态规划数组 dp 用于存储从 0 到 amount 每个金额所需的最小硬币数。

    • 初始化时,dp[0] = 0,表示凑出金额0不需要任何硬币。
    • 对于其他金额 i,初始化为 float('inf'),表示无法通过当前硬币组合凑出该金额。
  2. 遍历硬币面值

    • 对每个硬币面值,更新 dp 数组。对于每个金额 i,判断如果使用当前硬币 coin 是否能得到更少的硬币数,即 dp[i] = min(dp[i], dp[i - coin] + 1)
  3. 回溯

    • 从 dp[amount] 开始回溯,找出具体使用的硬币组合。每次回溯时,查找满足 dp[current_amount] == dp[current_amount - coin] + 1 的硬币,并减去该硬币的面值。

复杂度分析:

  • 时间复杂度O(amount * n),其中 n 是硬币的种类数,amount 是目标金额。对于每个硬币,遍历从 coin 到 amount 的所有金额,更新 dp 数组。
  • 空间复杂度O(amount)dp 数组的大小为 amount + 1

结果示例:

对于输入:

  • coins = [1, 2, 5] 和 amount = 18,输出为 [5, 5, 5, 2, 1]
  • coins = [1, 3, 4] 和 amount = 6,输出为 [3, 3]
  • coins = [5] 和 amount = 10,输出为 [5, 5]

这个方法通过动态规划求得最少硬币数,并且通过回溯找到具体的硬币组合。