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

103 阅读4分钟

问题描述

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

解题思路

本问题属于动态规划问题,目标是最小化硬币数量,同时构造出具体组合。

  1. 定义状态
    • 使用数组 dp[i] 表示凑出金额 i 所需的最少硬币数。
    • 用辅助数组 coin_used[i] 记录凑出金额 i 时最后使用的硬币。
  2. 状态转移方程
    • 对于每个硬币 coin
dp[x]=min(dp[x],dp[xcoin]+1)dp[x]=min(dp[x],dp[x−coin]+1)

其中,dp[x - coin] + 1 表示凑出金额 x 所需的硬币数量。

  1. 初始化
    • dp[0] = 0(凑出金额 0 不需要硬币)。
    • dp[i] = ∞(其他金额初始为无穷大,表示暂时不可达)。
  2. 组合构造
    • 根据 coin_used 数组从金额 amount 开始反推,逐步找到构造方案。
  3. 返回结果
    • 如果 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]

分析:

  1. 初始化:
  • dp = [0, inf, inf, ..., inf](长度为 19。
  • coin_used = [-1, -1, -1, ..., -1](长度为 19)。
  1. 动态规划过程:
  • 遍历硬币 1
    • 对于每个金额 x118
      • 更新 dp[x] = dp[x - 1] + 1,即 dp[x] = x(因为只用硬币 1 时,每个金额需要的硬币数等于金额大小)。
      • 记录硬币 coin_used[x] = 1
  • 遍历硬币 2
    • 对于每个金额 x218
      • 更新 dp[x] = min(dp[x], dp[x - 2] + 1)
        • 如果用硬币 2 凑出的硬币数更少,更新 dp[x],并记录硬币 coin_used[x] = 2
  • 遍历硬币 5
    • 对于每个金额 x518
      • 更新 dp[x] = min(dp[x], dp[x - 5] + 1)
        • 如果用硬币 5 凑出的硬币数更少,更新 dp[x],并记录硬币 coin_used[x] = 5
  1. 结果构造:
  • 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]

分析:

  1. 初始化:
  • dp = [0, inf, inf, inf, inf, inf, inf](长度为 7)。
  • coin_used = [-1, -1, -1, -1, -1, -1, -1]
  1. 动态规划过程:
  • 遍历硬币 1
    • 更新 dp[x]x,记录 coin_used[x] = 1
  • 遍历硬币 3
    • 对于金额 36
      • 更新 dp[x] = min(dp[x], dp[x - 3] + 1)
        • 使用硬币 336 时,更新为更优解,记录 coin_used[x] = 3
  • 遍历硬币 4
    • 对于金额 46
      • dp[4] 更新为 1dp[5]dp[6] 无法优化。
  1. 结果构造:
  • amount = 6 开始,根据 coin_used 回溯:
    • 使用硬币 3(6 → 3)。
    • 使用硬币 3(3 → 0)。
  • 组合为 [3, 3]

样例3:

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

分析:

  1. 初始化:
  • dp = [0, inf, inf, inf, ..., inf](长度为 11)。
  • coin_used = [-1, -1, -1, ..., -1]
  1. 动态规划过程:
  • 遍历硬币 5
    • 对于金额 510
    • dp[5] = 1coin_used[5] = 5
    • dp[10] = dp[5] + 1 = 2coin_used[10] = 5
  1. 结果构造:
  • amount = 10 开始,根据 coin_used 回溯:
    • 使用硬币 5(10 → 5)。
    • 使用硬币 5(5 → 0)。
  • 组合为 [5, 5]

复杂度分析

  1. 时间复杂度

    • 外层循环遍历硬币种类数 len(coins)
    • 内层循环遍历金额范围 amount
    • 因此时间复杂度为 O(len(coins)×amount)
  2. 空间复杂度

    • 动态数组 dpcoin_usedO(amount) 空间。