625. 最大公约数求和(Gcd Sum)
定义
$$ G(N)=\sum_{j=1}^{N}\sum_{i=1}^{j}\gcd(i,j) $$已知 \(G(10)=122\)。求 \(G(10^{11})\),并将结果对 \(998244353\) 取模。
分析:本题的直接枚举会超时,关键在于把双重求和改写成“欧拉函数前缀和 + 整除分块”模型。核心难点是快速计算 \(\Phi(n)=\sum_{k=1}^{n}\varphi(k)\)。
一、数学背景
先固定外层的 \(j\),考虑
$$
F(j)=\sum_{i=1}^{j}\gcd(i,j)
$$
可以用经典分解式得到
$$
F(j)=\sum_{d\mid j} d\cdot \varphi(j/d)
$$
于是原式变为
$$
G(N)=\sum_{j=1}^{N}\sum_{d\mid j} d\cdot\varphi(j/d)
$$
令 \(j=dm\),得到
$$
G(N)=\sum_{m=1}^{N}\varphi(m)\sum_{d=1}^{\lfloor N/m\rfloor} d
=\sum_{m=1}^{N}\varphi(m)\cdot T(\lfloor N/m\rfloor)
$$
其中
$$
T(x)=\frac{x(x+1)}{2}
$$
这样问题转成: 1. 频繁访问 \(\Phi(x)=\sum_{k\le x}\varphi(k)\); 2. 对 \(\lfloor N/m\rfloor\) 做整除分块。
二、算法设计
1) 整除分块
对任意固定的商 \(q=\lfloor N/l\rfloor\),满足该商不变的区间是
$$
r=\left\lfloor\frac{N}{q}\right\rfloor
$$
于是区间 \([l,r]\) 的贡献为
$$
(\Phi(r)-\Phi(l-1))\cdot T(q)
$$
总区间数约为 \(O(\sqrt N)\)。
2) 递推计算 \(\Phi(n)\)
利用恒等式
$$
\sum_{k=1}^{n}\varphi(k)\left\lfloor\frac{n}{k}\right\rfloor=\frac{n(n+1)}{2}
$$
整理可得
$$
\Phi(n)=\frac{n(n+1)}{2}-\sum_{l=2}^{n}(r-l+1)\cdot \Phi(\lfloor n/l\rfloor)
$$
同样用整除分块处理上式,并用记忆化缓存重复子问题。
3) 小范围筛预处理
对较小的 \(n\)(文中实现取 5_000_000)先用线性筛求 \(\varphi\) 与其前缀和,作为递推的“地基”,大幅减少递归深度与重复开销。
三、复杂度分析
- 线性筛部分:时间 \(O(L)\),空间 \(O(L)\),其中 \(L=5\times 10^6\)。
- 主求解部分:整除分块 + 记忆化递推,实际状态数约 \(O(\sqrt N)\) 到 \(O(N^{2/3})\) 之间,实测可在 60 秒内完成。
- 对本题目标规模 \(N=10^{11}\),本地 3 次实测均低于 60 秒。
四、代码实现与说明
"""Project Euler 625 - Gcd Sum."""
from __future__ import annotations
from functools import lru_cache
from time import perf_counter
MOD = 998244353
TARGET_N = 10**11
# Benchmarked on this machine to keep the full solve under 60 seconds.
SIEVE_LIMIT = 5_000_000
def build_phi_prefix(limit: int, mod: int) -> list[int]:
"""Build prefix sums of Euler's totient up to `limit` modulo `mod`."""
phi = [0] * (limit + 1)
is_composite = bytearray(limit + 1)
primes: list[int] = []
phi[1] = 1
for i in range(2, limit + 1):
if not is_composite[i]:
primes.append(i)
phi[i] = i - 1
for p in primes:
v = i * p
if v > limit:
break
is_composite[v] = 1
if i % p == 0:
phi[v] = phi[i] * p
break
phi[v] = phi[i] * (p - 1)
prefix = [0] * (limit + 1)
prefix[1] = 1
for i in range(2, limit + 1):
prefix[i] = (prefix[i - 1] + phi[i]) % mod
return prefix
def build_solver(mod: int = MOD, sieve_limit: int = SIEVE_LIMIT):
"""Prepare reusable closures for Phi-prefix and G(n) queries."""
prefix_small = build_phi_prefix(sieve_limit, mod)
inv2 = pow(2, mod - 2, mod)
@lru_cache(maxsize=None)
def phi_prefix_sum(n: int) -> int:
if n <= sieve_limit:
return prefix_small[n]
total = (n % mod) * ((n + 1) % mod) % mod
total = (total * inv2) % mod
l = 2
while l <= n:
q = n // l
r = n // q
count = r - l + 1
total = (total - (count % mod) * phi_prefix_sum(q)) % mod
l = r + 1
return total
def gcd_sum(n: int) -> int:
ans = 0
l = 1
while l <= n:
q = n // l
r = n // q
phi_interval = (phi_prefix_sum(r) - phi_prefix_sum(l - 1)) % mod
tri = (q % mod) * ((q + 1) % mod) % mod
tri = (tri * inv2) % mod
ans = (ans + phi_interval * tri) % mod
l = r + 1
return ans
return gcd_sum
def main() -> None:
build_start = perf_counter()
gcd_sum = build_solver()
build_elapsed = perf_counter() - build_start
print(f"build_elapsed_seconds = {build_elapsed:.3f}")
sample = gcd_sum(10)
print(f"G(10) mod {MOD} = {sample}")
assert sample == 122, f"Sample check failed: got {sample}, expected 122"
t0 = perf_counter()
result = gcd_sum(TARGET_N)
elapsed = perf_counter() - t0
print(f"G(10^11) mod {MOD} = {result}")
print(f"solve_elapsed_seconds = {elapsed:.3f}")
if __name__ == "__main__":
main()
代码说明(按执行顺序):
常量区
MOD是题目要求的模数。TARGET_N是目标输入规模。SIEVE_LIMIT是性能调优后用于小范围预处理的阈值。
build_phi_prefix
phi存储每个整数的欧拉函数值。is_composite与primes配合实现线性筛。- 外层循环枚举
i,若i未标记则为新质数,设置phi[i]=i-1。 - 内层用已知质数扩展到
v=i*p,并按i%p==0与否分别套用线性筛更新公式。 - 最后构建
prefix,使prefix[x]=sum_{k<=x}phi(k) (mod MOD),供后续快速查询。
build_solver
- 先调用
build_phi_prefix得到小范围前缀和。 - 计算
inv2(2 的模逆),用于三角数公式中的除 2。 - 定义带
lru_cache的phi_prefix_sum,用于递推计算大范围Phi(n)。
phi_prefix_sum
- 若
n<=sieve_limit,直接返回预处理表值。 - 否则先用
n(n+1)/2初始化。 - 对
floor(n/l)的等值区间[l,r]分块,批量减去(r-l+1)*Phi(floor(n/l))。 - 每个
n只会被缓存计算一次,避免重复递归。
gcd_sum
- 这是主函数,按
floor(n/l)分块遍历[l,r]。 - 区间欧拉和为
Phi(r)-Phi(l-1)。 - 同商值对应的三角数
T(q)只算一次。 - 累加区间贡献后推进
l=r+1。
main
- 先构建求解器并输出预处理耗时。
- 运行样例
G(10)并断言校验。 - 再计算目标规模并输出求解耗时。