問題
乗法的関数 $f$ を、素数 $p$ と $e\ge1$ について $f(p^e)=p\oplus e$($\oplus$ はビットごとの XOR)、$f(1)=1$ で定める。
$$\sum_{n=1}^{N}f(n)\bmod(10^9+7)$$
を求めよ。
入力形式
N
制約
| # | 制約 |
|---|---|
| 1 | $1\le N\le10^{10}$ |
| 2 | 制限時間 5 秒を想定(下の計算量の注を参照) |
入出力例
入力例1
10出力例1
36($f(1..10)=1,3,2,0,4,6,6,1,1,12$。$f(4)=2\oplus2=0$、$f(8)=2\oplus3=1$、$f(9)=3\oplus2=1$)
入力例2
100出力例2
3106入力例3
10000000000出力例3
251539221($N=10^{10}$ の値は、本解と、別に実装した min_25 篩の 2 つで一致。入力例 1・2 と $N\le3\times10^5$ の 120 個は篩による全探索とも一致)
背景と出典
- $f(p^e)=p\oplus e$ の総和は LOJ #6053「简单的函数」と記憶している(原典は本セッションで未確認)。そこでは min_25 篩で解くのが定番だったと記憶している
- 強力数法そのものは Day118-Q2 で扱った。杜教篩($\varphi$ の前綴和)は Day100-Q4。本問はその 2 つを組み合わせ、「$g$ をどう選ぶか」と「どちらが律速か」を測ることに焦点を置く
- 本問の問題文・制約・入力例は自作(関数の定義だけが上の問題と同じ)
概念図
ヒント(クリックで段階的に開示)
ヒント1(方向性)
$f=g*h$(Dirichlet 積)と分解できて、$g(p)=f(p)$ がすべての素数で成り立つなら、$h(p)=0$ になる。乗法的な $h$ が素数で 0 なら、$h(n)\ne0$ になりうるのは強力数(素因数がすべて 2 乗以上で入っている数)だけ。$N$ 以下の強力数は $O(\sqrt N)$ 個しかない。
ヒント2(アプローチ)
$\sum_{n\le N}f(n)=\sum_{m\text{ 強力数}}h(m)\,S_g(\lfloor N/m\rfloor)$。必要なのは
- $S_g$ が速く求まる $g$ で、$g(p)=p\oplus1$
- 各素数 $p\le\sqrt N$ での $h(p^e)$($f=g*h$ から $h(p^e)=f(p^e)-\sum_{i\ge1}g(p^i)h(p^{e-i})$)
ヒント3(誘導)
- 奇素数では $p\oplus1=p-1=\varphi(p)$。$p=2$ では $3=\varphi(2)+2$
- $u(2)=2$、$u$ はそれ以外の素数べきで 0 という乗法的関数を作り、$g=\varphi*u$ とする。すると $g(2)=\varphi(2)+u(2)=3$、奇素数では $g(p)=\varphi(p)$
- $S_g(m)=S_\varphi(m)+2S_\varphi(\lfloor m/2\rfloor)$。$S_\varphi$ は杜教篩 $S_\varphi(m)=\frac{m(m+1)}2-\sum_{d=2}^{m}S_\varphi(\lfloor m/d\rfloor)$ で、$\lfloor N/k\rfloor$ の形の値だけ求めればよい
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
MOD = 10**9 + 7
def main():
N = int(sys.stdin.readline())
# ---- 1) φ の前計算(篩): T ≈ 0.5·N^(2/3)(下の測定で決めた)
T = min(N, max(100, int(0.5 * N ** (2 / 3))))
phi = list(range(T + 1))
comp = bytearray(T + 1)
for p in range(2, T + 1):
if not comp[p]:
comp[p * p::p] = b"\x01" * len(range(p * p, T + 1, p)) if p * p <= T else b""
for j in range(p, T + 1, p):
phi[j] -= phi[j] // p
pre = [0] * (T + 1)
acc = 0
for i in range(1, T + 1):
acc += phi[i]
pre[i] = acc % MOD
# ---- 2) 杜教篩: m > T の S_φ(m) を m = N//k の k で memo
big = {}
inv2 = (MOD + 1) // 2
def S_phi(m):
if m <= T:
return pre[m]
k = N // m
if k in big:
return big[k]
# 小さい m から順に計算すれば再帰は浅い(下で k の大きい順に呼ぶ)
res = m % MOD * ((m + 1) % MOD) % MOD * inv2 % MOD
d = 2
while d <= m:
q = m // d
nd = m // q
res -= (nd - d + 1) * S_phi(q)
d = nd + 1
res %= MOD
big[k] = res
return res
for k in range(N // (T + 1), 0, -1): # m = N//k が大きくなる順 → 再帰は 1 段で済む
if N // k > T:
S_phi(N // k)
def S_g(m): # g = φ * u、u(2) = 2 だけ非零 → S_g(m) = S_φ(m) + 2 S_φ(m//2)
return (S_phi(m) + 2 * S_phi(m // 2)) % MOD
# ---- 3) 素数 p <= √N ごとに h(p^e) を作る
r = int(N ** 0.5)
while r * r > N:
r -= 1
while (r + 1) * (r + 1) <= N:
r += 1
sieve = bytearray([1]) * (r + 1)
primes = []
for i in range(2, r + 1):
if sieve[i]:
primes.append(i)
sieve[i * i::i] = bytearray(len(range(i * i, r + 1, i)))
hcoef = []
for p in primes:
pe = [1]
while pe[-1] <= N // p:
pe.append(pe[-1] * p)
E = len(pe) - 1
if p == 2:
g = [1] + [3 if e == 1 else pe[e] for e in range(1, E + 1)]
else:
g = [1] + [pe[e] - pe[e - 1] for e in range(1, E + 1)]
h = [1, 0]
for e in range(2, E + 1):
v = p ^ e
for i in range(1, e + 1):
v -= g[i] * h[e - i]
h.append(v % MOD)
hcoef.append(h)
# ---- 4) 強力数を DFS で列挙し Σ h(n) S_g(N//n)
ans = S_g(N)
stack = [(1, 1, 0)] # (n, h(n), 次に使える素数の添字)
while stack:
n, hn, i = stack.pop()
while i < len(primes):
p = primes[i]
if n * p * p > N:
break
h = hcoef[i]
m = n * p * p
e = 2
while m <= N:
hv = hn * h[e] % MOD
if hv:
ans += hv * S_g(N // m)
stack.append((m, hv, i + 1))
m *= p
e += 1
i += 1
print(ans % MOD)
main()
計算量: 篩 $O(T\log\log T)$、杜教篩 $O(N/\sqrt T)$、強力数 $O(\sqrt N)$ 個。$T\approx N^{2/3}$ で全体 $O(N^{2/3})$。$N=10^8$: 0.15 s、$10^9$: 0.67 s、$10^{10}$: 3.56 s・214 MB、$10^{11}$: 17.46 s・923 MB(Python 3.14.8、Apple M5 Pro、負荷平均 12〜20 での実測、起動込み)。$N=10^{10}$ の内訳は篩 0.75 s、杜教篩 2.0 s、$h$ の表 0.03 s、強力数の DFS 0.11 s(実測)。2 秒には収まらないので制限時間 5 秒を想定とした。
Step-by-Step 解説
1なぜ強力数だけでよいか
$f=g*h$ で $f(p)=g(p)h(1)+g(1)h(p)=g(p)+h(p)$。$g(p)=f(p)$ なら $h(p)=0$。$h$ は乗法的なので、素因数 $p$ がちょうど 1 乗で入る $n$ では $h(n)=0$。残るのは強力数だけで、$N=10^{10}$ 以下に 214122 個(1 を含む、実測)。
2$g$ の選び方
$f(p)=p\oplus1$ は「$p$ が奇数なら最下位ビットを落とす $=p-1$」「$p=2$ なら $3$」。$\varphi(p)=p-1$ が奇素数で一致するので、2 だけ直す。$g=\varphi*u$、$u(2)=2$(他の素数べきで 0)とすると
- $g(2)=\varphi(2)+2\varphi(1)=3$、$g(2^e)=\varphi(2^e)+2\varphi(2^{e-1})=2^e$($e\ge2$)
- 奇素数べきでは $g(p^e)=\varphi(p^e)$
- 前綴和は $S_g(m)=\sum_{k}u(k)S_\varphi(\lfloor m/k\rfloor)=S_\varphi(m)+2S_\varphi(\lfloor m/2\rfloor)$
$\lfloor\lfloor N/n\rfloor/2\rfloor=\lfloor N/(2n)\rfloor$ なので、要る値はすべて $\lfloor N/k\rfloor$ の形に収まる。
3$h(p^e)$
$f(p^e)=\sum_{i=0}^{e}g(p^i)h(p^{e-i})$ を $h(p^e)$ について解く。$p=2$ で $h=1,0,-4,5,-9,14,\dots$、$p=3$ で $1,0,-5,-8,-1,\dots$(実測)。負の値は法を取って持つ。
4杜教篩
$\sum_{d\mid n}\varphi(d)=n$ から $\sum_{d=1}^{m}S_\varphi(\lfloor m/d\rfloor)=\frac{m(m+1)}2$。$m\le T$ は篩の前綴和、$m>T$ は $k=\lfloor N/m\rfloor$ を鍵に memo。$k$ の大きい順($m$ の小さい順)に呼ぶと、右辺の $S_\varphi(\lfloor m/d\rfloor)$ はすでに計算済みで再帰が 1 段で終わる。
5$T$ を測って決める
理論上の最適は $T\approx N^{2/3}$ だが、Python では篩(リストの書き換え)と杜教篩(整数の割り算の繰り返し)の定数が違う。$N=10^{10}$ で $T=c\cdot N^{2/3}$ の $c$ を振ると、$c=0.15$: 4.49 s、$0.3$: 3.71 s、$0.5$: 3.48 s、$1.0$: 4.06 s(実測)。$c=0.5$ を採った。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 線形篩で $f$ を全部作る | $O(N)$ | 検算用のみ | $N=10^{10}$ では不可能 |
| min_25 篩(素数の個数と和を Lucy 法で、合成数を再帰で) | $O(N^{3/4}/\log N)$ | 比較用に実装 | 答えは一致。$10^{10}$ で 3.86 s・57 MB、$10^{11}$ で 19.62 s・150 MB(実測)。速さは本解とほぼ同じで、メモリは本解の 1/4〜1/6 |
| $g=\varphi$ のまま強力数法 | $O(N^{2/3})$ | 不採用(誤り) | $h(2)=2\ne0$ になり、$h$ が強力数以外にも乗る。$N=1..300$ で 299 個誤る(実測。正しいのは $N=1$ だけ) |
| 強力数法+杜教篩(本解) | $O(N^{2/3})$ | 採用 | 3.56 s。$g$ の選び方と律速の位置が目に見える |
前提が変わったら
- メモリが 256 MB 未満なら min_25 篩を選ぶ($10^{10}$ で 57 MB 対 214 MB)
- $S_g$ が閉じた式で求まる $g$(例: $f(p)=p$ なら $g=\mathrm{id}$、$S_g=m(m+1)/2$)なら、律速の杜教篩が消えて強力数の DFS($10^{10}$ で 0.11 s)だけになり、強力数法が圧勝する。本問の $f(p)=p-1$ は $\varphi$ が要るので、その利点が出ない
- $N=10^{11}$ 以上ではどちらも Python では 17〜20 s。C++ で書くか、$\varphi$ の前綴和を事前計算の表から読む必要がある
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| $g=\varphi$ で済ませる | $f(2)=3$ を見落とす($2\oplus1=3$ で $p-1$ ではない) | 2 だけ補正した $g=\varphi*u$ |
| $f(p^e)$ を $p^e\oplus e$ と読む | 定義の読み違い | $p\oplus e$。$f(4)=0$、$f(8)=1$ |
| $S_g(\lfloor N/n\rfloor)$ の $\lfloor\cdot/2\rfloor$ を別の値として memo する | 鍵の形を揃えない | $\lfloor N/(2n)\rfloor$ も $\lfloor N/k\rfloor$ の形。$k=\lfloor N/m\rfloor$ を鍵にする |
| 杜教篩を $m$ の大きい順に再帰で呼ぶ | 再帰が深くなり Python の上限に当たる | $k$ の大きい順($m$ の小さい順)に先に埋める |
| $h(p^e)$ を法を取らずに持つ | 負の値を許すつもりで巨大化 | 毎回 $\bmod$ を取る |
次のステップ
- 発展1: $f(p^e)=p^e\oplus e$ のように、$f(p)$ が $p$ の多項式で書けない関数で強力数法が使えるかを考える
- 発展2: min_25 篩の Lucy 部分だけを本解の $S_\varphi$ の代わりに使い($S_g$ を素数和から作る道)、どちらの組み合わせが速いか測る
- 発展3: 同じ $N$ で 1000 個の $f$($f(p^e)=p\oplus(e+c)$ など)を一度に答える場合、杜教篩を共有して何倍速くなるか
検証ログ(本セッションで実行した数値)
- 正しさ(実測): $N=1..59$、乱数 60 個($60\le N\le3\times10^5$)、$N=3\times10^5$ の計 120 個で、最小素因数の篩で $f$ を全部作った全探索と一致($N=3\times10^5$ の値 396335019)。$N=10,100,3\times10^5,10^8,10^9,10^{10},10^{11}$ で min_25 篩と一致
- 誤った方法(実測): $g=\varphi$ のままでは $N=1..300$ の 299 個で誤る
- 時間とメモリ(実測、起動込み、負荷平均 12〜20):
| $N$ | 本解 | min_25 篩 |
|---|---|---|
| $10^8$ | 0.15 s・25 MB | 0.15 s・20 MB |
| $10^9$ | 0.67 s・59 MB | 0.73 s・29 MB |
| $10^{10}$ | 3.56 s・214 MB | 3.86 s・57 MB |
| $10^{11}$ | 17.46 s・923 MB | 19.62 s・150 MB |
- 内訳(実測、$N=10^{10}$): 篩 0.75 s、杜教篩 2.0 s(memo 4308 個)、強力数の DFS 0.11 s(強力数 214121 個+1)
- 扱っていない範囲: 原典(LOJ)の確認、C++ での比較、numpy を使った篩(この環境の Python 3.14 には numpy が無かった)