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

64 阅读4分钟

题目内容

小C有多种不同面值的硬币,每种硬币的数量是无限的。他希望知道,如何使用最少数量的硬币,凑出给定的总金额N。小C对硬币的组合方式很感兴趣,但他更希望在满足总金额的同时,使用的硬币数量尽可能少。

例如:小C有三种硬币,面值分别为 125。他需要凑出总金额为 18。一种最优的方案是使用三个 5 面值的硬币,一个 2 面值的硬币和一个 1 面值的硬币,总共五个硬币。

题目分析

问题理解

我们需要找到一种方法,使用最少数量的硬币凑出给定的总金额。这个问题可以通过动态规划来解决。动态规划的核心思想是将问题分解为子问题,并存储子问题的解,以避免重复计算。

数据结构选择

我们使用一个一维数组 dp 来存储每个金额所需的最少硬币数量。dp[i] 表示凑出金额 i 所需的最少硬币数量。

算法步骤

  1. 初始化

    • 创建一个长度为 amount + 1 的数组 dp,并将所有元素初始化为 float('inf'),表示初始状态下每个金额所需的硬币数量为无穷大。
    • 设置 dp[0] = 0,因为凑出金额为 0 时所需的硬币数量为 0。
  2. 动态规划

    • 遍历每种硬币面值 coin
    • 对于每个硬币面值 coin,遍历从 coin 到 amount 的所有金额 i
    • 更新 dp[i] 为 min(dp[i], dp[i - coin] + 1),表示使用当前硬币面值时,凑出金额 i 所需的最少硬币数量。
  3. 回溯

    • 如果 dp[amount] 仍然是 float('inf'),表示无法凑出目标金额,返回空列表。
    • 否则,从 amount 开始回溯,确定使用的硬币组合。
    • 遍历每种硬币面值,找到使得 dp[i] == dp[i - coin] + 1 的硬币,并将其加入结果列表。
  4. 排序

    • 对结果列表进行排序,使其从大到小排列。

具体例子

例子1

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

步骤

  1. 初始化 dp 数组:dp = [0, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf]

  2. 使用硬币面值 1

    • dp = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
  3. 使用硬币面值 2

    • dp = [0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9]
  4. 使用硬币面值 5

    • dp = [0, 1, 1, 2, 2, 1, 2, 2, 3, 3, 2, 3, 3, 4, 4, 3, 4, 4, 5]
  5. 回溯:

    • dp[18] = 5,表示需要 5 个硬币。
    • 回溯路径:18 -> 13 -> 8 -> 3 -> 1 -> 0,使用的硬币为 [5, 5, 5, 2, 1]
  6. 排序:[5, 5, 5, 2, 1]

输出[5, 5, 5, 2, 1]

例子2

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

步骤

  1. 初始化 dp 数组:dp = [0, inf, inf, inf, inf, inf, inf]

  2. 使用硬币面值 1

    • dp = [0, 1, 2, 3, 4, 5, 6]
  3. 使用硬币面值 3

    • dp = [0, 1, 2, 1, 2, 3, 2]
  4. 使用硬币面值 4

    • dp = [0, 1, 2, 1, 1, 2, 2]
  5. 回溯:

    • dp[6] = 2,表示需要 2 个硬币。
    • 回溯路径:6 -> 3 -> 0,使用的硬币为 [3, 3]
  6. 排序:[3, 3]

输出[3, 3]

例子3

输入coins = [5], amount = 10

步骤

  1. 初始化 dp 数组:dp = [0, inf, inf, inf, inf, 1, inf, inf, inf, inf, 2]

  2. 使用硬币面值 5

    • dp = [0, inf, inf, inf, inf, 1, inf, inf, inf, inf, 2]
  3. 回溯:

    • dp[10] = 2,表示需要 2 个硬币。
    • 回溯路径:10 -> 5 -> 0,使用的硬币为 [5, 5]
  4. 排序:[5, 5]

输出[5, 5]

参考代码

def solution(coins, amount):
    dp = [float('inf')] * (amount + 1)  # 初始化 dp 列表
    dp[0] = 0  # 0 总额需要 0 个硬币

    for coin in coins:  # 遍历每种硬币面值
        for i in range(coin, amount + 1):  # 完全背包计算
            dp[i] = min(dp[i], dp[i - coin] + 1)  # 更新 dp 列表

    if dp[amount] == float('inf'):  # 如果无法达到目标总额
        return []

    result = []
    i = amount
    while i > 0:  # 回溯确定使用的硬币
        for coin in coins:
            if i >= coin and dp[i] == dp[i - coin] + 1:
                result.append(coin)
                i -= coin
                break
    result.sort(reverse=True)
    return result

总结

通过动态规划和回溯的方法,我们可以有效地找到使用最少数量的硬币凑出给定总金额的方案。动态规划部分用于计算每个金额所需的最少硬币数量,回溯部分用于确定具体的硬币组合。最后,对结果列表进行排序,使其从大到小排列。