Files
SolutionEuler/solutions/0031
Sidney Zhang 8ee312ac1d feat:添加欧拉项目第30题和第31题的解决方案
📝 docs:为第31题添加详细的动态规划算法说明文档
2025-12-29 18:18:10 +08:00
..

动态规划法

核心思路

这个函数采用动态规划的思想,通过构建一个 ways 数组来累积计算每个金额对应的方法数。


逐行解析

1. 初始化

coins = [1, 2, 5, 10, 20, 50, 100, 200]
ways = [1] + [0] * 200  # 结果是 [1, 0, 0, 0, ..., 0] (共201个元素)
  • coins: 所有可用的硬币面值(单位:便士)
  • ways[i]: 表示凑成金额 i 的方法数
  • 关键点ways[0] = 1 表示凑成0元有1种方法什么都不用这是动态规划的基准条件

2. 双重循环的核心逻辑

for coin in coins:           # 按顺序遍历每种硬币
    for i in range(coin, 201):  # 从当前硬币面值遍历到200
        ways[i] += ways[i - coin]

这就是状态转移方程,其含义是:

凑成金额 i 的方法数 = 原来方法数 + 使用当前硬币的方法数

其中 ways[i - coin] 表示在使用了1枚当前硬币后凑齐剩余金额的方法数。


具体执行过程演示

让我们跟踪计算前几个值的变化,以理解算法如何工作:

初始状态

ways = [1, 0, 0, 0, 0, 0, ...]  # ways[0]=1

第1轮使用硬币 1p

for i in range(1, 201):
    ways[i] += ways[i-1]
  • i=1: ways[1] += ways[0]0 + 1 = 1 ✓ 凑1p: {1}
  • i=2: ways[2] += ways[1]0 + 1 = 1 ✓ 凑2p: {1,1}
  • i=3: ways[3] += ways[2]0 + 1 = 1 ✓ 凑3p: {1,1,1}
  • ...
  • 结果只用1p硬币每个金额都有且仅有1种方法

第2轮加入硬币 2p

for i in range(2, 201):
    ways[i] += ways[i-2]

关键更新点:

  • i=2: ways[2] += ways[0]1 + 1 = 2
    • 原来:{1,1}
    • 新增:{2}
  • i=3: ways[3] += ways[1]1 + 1 = 2
    • 原来:{1,1,1}
    • 新增:{1,2}
  • i=4: ways[4] += ways[2]1 + 2 = 3
    • 原来:{1,1,1,1}
    • 新增:{1,1,2}, {2,2}

第3轮加入硬币 5p

当加入5p硬币后凑5p的方法从1种 {1,1,1,1,1} 变成2种新增 {5}。

以此类推,直到处理完所有硬币类型。


为什么这样计算?

这个算法的精妙之处在于:

  1. 按硬币顺序处理:确保计算的是组合数(不考虑顺序)

    • {1,2,2} 和 {2,1,2} 视为同一种方法
    • 如果调换循环顺序(先金额后硬币),会计算出排列数
  2. 累积效应ways 数组保存的是所有已处理硬币能凑成的方法总数

  3. 避免重复:每种硬币只被考虑一次在其对应的外层循环中,确保不重复计算


最终结果

当循环结束后,ways[200] 的值就是 200便士能被凑成的所有方法数。

print(f"\nThe number of ways to make £2 is {ways[200]:,d}")

输出结果是:73682

这意味着用这8种英国硬币凑成2英镑共有73,682种不同的方式。


时间复杂度

  • O(N×M):其中 N = 硬币种类数8M = 目标金额200
  • 实际计算量约为 8 × 200 = 1600 次操作,效率极高

这个算法简洁优雅,展示了动态规划在组合计数问题中的强大威力。