127. abc击中数(abc-hits)
\(n\)的根数(radical),记为\(\operatorname{rad}(n)\),是\(n\)的不同质因子的乘积。例如,\(504 = 2^3 \times 3^2 \times 7\),所以\(\operatorname{rad}(504) = 2 \times 3 \times 7 = 42\)。
若正整数三元组\((a, b, c)\)满足以下四个条件,则称其为abc击中数(abc-hit):
- \(\gcd(a, b) = \gcd(a, c) = \gcd(b, c) = 1\)
- \(a < b\)
- \(a + b = c\)
- \(\operatorname{rad}(abc) < c\)
例如\((5, 27, 32)\)是一个abc击中数,因为: - 三个最大公约数条件成立 - \(5 < 27\) - \(5 + 27 = 32\) - \(\operatorname{rad}(4320) = 30 < 32\)
abc击中数相当稀少,对于\(c < 1000\),仅有三十一个abc击中数,且\(\sum c = 12523\)。
求对于\(c < 120000\)的\(\sum c\)。
一、数学背景
本题涉及数论中的根数(radical)概念和abc猜想的相关组合结构。
关键性质1——互质性的简化: 由于\(a + b = c\),三个\(\gcd\)条件实际上退化为一个: - \(\gcd(a, c) = \gcd(a, a+b) = \gcd(a, b)\) - \(\gcd(b, c) = \gcd(b, a+b) = \gcd(b, a)\) 因此只需验证\(\gcd(a, b) = 1\)即可保证三项两两互质。
关键性质2——根数的可乘性: 对于两两互质的\(a, b, c\),有\(\operatorname{rad}(abc) = \operatorname{rad}(a) \cdot \operatorname{rad}(b) \cdot \operatorname{rad}(c)\)。这是因为不同数的不同质因子之间没有交集。
因此条件4变为:\(\operatorname{rad}(a) \cdot \operatorname{rad}(b) \cdot \operatorname{rad}(c) < a + b\)。
关键性质3——搜索空间的约束: 由条件4可得\(\operatorname{rad}(a) \cdot \operatorname{rad}(b) < a + b < 120000\)。这意味着两个数的根数之积有严格上界,大幅限制了需要搜索的\((a, b)\)对。
二、算法设计
步骤1:筛法计算根数。 使用类似埃拉托斯特尼筛法的方式,对于每个质数\(p\),将其所有倍数乘以\(p\),最终得到每个数的\(\operatorname{rad}(n)\)。
步骤2:按根数值分组。 将所有\(n\)(\(1 \le n < 120000\))按其根数值归类到字典中。这使得我们可以按根数对而非逐个数值来搜索。
步骤3:在根数对上搜索。 对于满足\(r_a \cdot r_b < 120000\)的每对根数值\((r_a, r_b)\): - 遍历根数为\(r_a\)的所有\(a\)值 - 遍历根数为\(r_b\)的所有\(b\)值(要求\(b > a\)) - 计算\(c = a + b\),检查\(c < 120000\) - 验证\(\gcd(a, b) = 1\) - 验证\(r_a \cdot r_b \cdot \operatorname{rad}(c) < c\)
为什么根数分组有效: 约束\(r_a \cdot r_b < 120000\)意味着两个根数不能同时很大——至少一个小于\(\sqrt{120000} \approx 346\)。对于大根数值,能与之配对的根数范围非常有限,这极大地剪枝了搜索空间。
三、复杂度分析
- 时间复杂度:筛法\(O(N \log \log N)\);根数对搜索的实际操作次数取决于根数约束的剪枝效果。对于\(N = 120000\),实测仅需约2.7秒
- 空间复杂度:\(O(N)\)用于存储根数数组和分组字典
- 算法在样例\(c < 1000\)上正确验证(得到31个击中数,\(\sum c = 12523\))
四、代码实现与说明
#!/usr/bin/env python3
"""
Project Euler Problem 127: abc-hits
"""
import time
def compute_radicals(limit):
"""Compute rad(n) for all n from 1 to limit using a sieve."""
rad = [1] * (limit + 1)
rad[0] = 0
for p in range(2, limit + 1):
if rad[p] == 1: # p is prime
step = p
for multiple in range(p, limit + 1, step):
rad[multiple] *= p
return rad
compute_radicals函数——筛法计算根数
- 初始化
rad数组,所有位置为1 - 遍历\(2\)到
limit,当rad[p] == 1时说明\(p\)为质数(尚未被任何质因子标记) - 对\(p\)的所有倍数,将
rad[multiple]乘以\(p\) - 最终
rad[n]等于\(n\)的所有不同质因子的乘积
def gcd(a, b):
"""Euclidean algorithm for greatest common divisor."""
while b:
a, b = b, a % b
return a
def solve(limit):
"""Find sum of c for abc-hits with c < limit."""
rad = compute_radicals(limit)
# Group numbers by radical value
groups = {}
for n in range(1, limit):
r = rad[n]
groups.setdefault(r, []).append(n)
radical_values = sorted(groups.keys())
total = 0
count = 0
for ra in radical_values:
max_rb = (limit - 1) // ra
list_a = groups[ra]
for rb in radical_values:
if rb > max_rb:
break
if ra * rb >= limit:
continue
list_b = groups[rb]
for a in list_a:
if a >= limit // 2:
continue
for b in list_b:
if b <= a:
continue
c = a + b
if c >= limit:
break
if ra * rb * rad[c] < c and gcd(a, b) == 1:
total += c
count += 1
print(f" Found {count} abc-hits")
return total
gcd函数
- 欧几里得算法的标准迭代实现,用于验证\(a\)与\(b\)互质
solve函数——根数分组
- 调用
compute_radicals计算所有数的根数 - 创建字典
groups,键为根数值,值为具有该根数的所有数组成的列表 - 提取所有不同的根数值并排序
solve函数——双层循环搜索
- 外层遍历所有根数值\(r_a\),计算\(r_b\)的最大允许值:
max_rb = (limit - 1) // r_a(由\(r_a \cdot r_b < limit\)导出) - 内层遍历不超过
max_rb的根数值\(r_b\) - 对
list_a中的每个\(a\)(需\(a < limit/2\)以保证\(a < b\)),遍历list_b中的每个\(b > a\) - 计算\(c = a + b\),检查\(c < limit\)(因为
list_b已排序,一旦\(c \ge limit\)即可跳出内层循环) - 验证\(r_a \cdot r_b \cdot \operatorname{rad}(c) < c\)和\(\gcd(a, b) = 1\)
- 满足条件则将\(c\)加入总和
def main():
limit = 120000
start = time.time()
answer = solve(limit)
elapsed = time.time() - start
print(f"Sum of c for c < {limit}: {answer}")
print(f"Time: {elapsed:.3f}s")
# Verify with sample test
sample_limit = 1000
sample_answer = solve(sample_limit)
print(f"Verification: sum for c < {sample_limit} = {sample_answer} (expected 12523)")
if __name__ == "__main__":
main()
main函数
- 以\(120000\)为上限调用
solve - 此外对\(c < 1000\)的样例进行验证,确保输出与题目给定的12523一致