feat:添加欧拉项目第30题和第31题的解决方案

📝 docs:为第31题添加详细的动态规划算法说明文档
This commit is contained in:
2025-12-29 18:18:10 +08:00
parent 25469e1022
commit 8ee312ac1d
3 changed files with 232 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
"""
Surprisingly there are only three numbers that can be written as the sum of fourth powers of their digits:
1634 = 1^4 + 6^4 + 3^4 + 4^4
8208 = 8^4 + 2^4 + 0^4 + 8^4
9474 = 9^4 + 4^4 + 7^4 + 4^4
As 1 = 1^4 is not a sum it is not included.
The sum of these numbers is 19316.
Find the sum of all the numbers that can be written as the sum of fifth powers of their digits.
"""
import time
from itertools import combinations_with_replacement
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} on ({kwargs}) taken: {end - start: .6f} seconds")
return result
return wrapper
def fifth_power(n: int, power: int = 5, strict: bool = False) -> tuple[bool, int]:
sumx = sum(int(digit) ** power for digit in str(n))
if strict:
isok = (sumx == n) and (power == len(str(sumx)))
return isok, sumx
return sumx == n, sumx
def top_number(n: int) -> int:
beg = (9**n) * n
beg_len = len(str(beg))
last = n
while True:
if beg_len >= n:
beg = (9**n) * beg_len
last = beg_len
beg_len = len(str(beg))
if beg_len == last:
break
else:
break
return int("9" * beg_len)
def is_match(num: int, comb: list[str]) -> bool:
if num in [1, 0]:
return False
numstr = str(num).zfill(len(comb))
numstr = sorted(numstr)
return numstr == sorted(comb)
@timer
def main_just_compute(power: int, strict: bool = False) -> None:
res = []
top = top_number(power)
for i in range(2, top + 1):
is_sum, sumx = fifth_power(i, power, strict)
if is_sum:
print(sumx, end="\t")
res.append(sumx)
print(f"\nThe sum is {sum(res):,d}")
@timer
def main_combinations(power: int) -> None:
top_len = len(str(top_number(power)))
res = set()
combs = combinations_with_replacement("0123456789", top_len)
for comb in combs:
tmp = sum(map(lambda x: int(x) ** power, comb))
if is_match(tmp, list(comb)):
print(tmp, end="\t")
res.add(tmp)
print(f"\nThe sum is {sum(res):,d}")
if __name__ == "__main__":
the_power = 5
main_just_compute(power=the_power, strict=False)
print("=" * 50)
main_combinations(power=the_power)

View File

@@ -0,0 +1,27 @@
"""
In the United Kingdom the currency is made up of pound (£) and pence (p).
There are eight coins in general circulation:
1p, 2p, 5p, 10p, 20p, 50p, £1 (100p), and £2 (200p).
It is possible to make £2 in the following way:
1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p
How many different ways can £2 be made using any number of coins?
"""
def main():
coins = [1, 2, 5, 10, 20, 50, 100, 200]
ways = [1] + [0] * 200
for coin in coins:
for i in range(coin, 201):
ways[i] += ways[i - coin]
print(f"\nThe number of ways to make £2 is {ways[200]:,d}")
if __name__ == "__main__":
main()

114
solutions/0031/readme.md Normal file
View File

@@ -0,0 +1,114 @@
# 动态规划法
## 核心思路
这个函数采用**动态规划**的思想,通过构建一个 `ways` 数组来累积计算每个金额对应的方法数。
---
## 逐行解析
### 1. 初始化
```python
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. 双重循环的核心逻辑
```python
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
```python
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
```python
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便士能被凑成的所有方法数。
```python
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 次操作,效率极高
这个算法简洁优雅,展示了动态规划在组合计数问题中的强大威力。