序文 §

面白いアルゴリズムを探すために、最近教えてもらったLibrary Checkerを眺めていたら素数計数関数 π(x)\pi(x)xxまでの素数を列挙するより速く計算する方法があることが判明したので紹介します。

Algorithm §

特に断りが無ければ、xxは実数とする。

以下ではaa番目の素数をpap_aとおく。1-indexedなのでp1=2p_1 = 2である。

自然数aaに対して、次の関数ϕ(x,a)\phi(x,a)を定義する。

ϕ(x,a)={nx:p (p is prime) [pnp>pa]} \phi(x,a) = |\{n \leq x : \forall p \ (p \ \mathrm{is \ prime}) \ [p |n \Rightarrow p \gt p_a]\}|

これはnnの素因数が「全て」pap_aよりも大きいようなnnの個数を指している。例えばϕ(10,2)={1,5,7}=3\phi(10,2) = |\{1,5,7\}| = 3である(a2=3a_2 = 3より、これより大きい素因数からなる10以下の数は5,7のみである、1は素数の約数を持たないことから前提が偽となり満たされる)。直感的にはエラトステネスの篩においてpap_aで篩にかけた後に残った整数の数に相当する。

また、非負整数kkと自然数aaに対して、次の関数Pk(x,a)P_k(x,a)を定義する。

Pk(x,a)={nx:n=q1q2qk (q1,,qk>pa)} P_k(x,a) = |\{n \leq x : n = q_1q_2\cdots q_k \ (q_1,\dots, q_k \gt p_a)\}|

これはつまり、pap_aより大きい素数をちょうどkk個素因数に持つ数の総数である。なお、便宜上P0(x,a)={1}=1P_0(x,a) = |\{1\}| = 1とする。

自明な式として次がある

ϕ(x,a)=k=0Pk(x,a)P1(x,a)=π(x)a \begin{aligned} \phi(x,a) &= \sum_{k=0}^\infty P_k(x,a) \cr P_1(x,a) &= \pi(x) - a \end{aligned}

よって、これを変形して次が導かれる。

π(x)=ϕ(x,a)+a1k=2Pk(x,a) \pi(x) = \phi(x,a) + a - 1 - \sum_{k=2}^\infty P_k(x,a)

ここでどんなaaに対しても、x<pakx \lt p_a^kとなるkkが必ず存在することから、Pk(x,a)P_k(x,a)に対してある添字が存在して、それより大きい添字ではPk(x,a)=0P_k(x,a) = 0となるため、この無限和は有限の値をとる。特にa=π(x1N)a = \pi(x^{\frac 1N})とすれば、Pk(x,a)P_k(x,a)kNk\geq Nにおいて0になる。

よって、Pk(x,a),ϕ(x,a)P_k(x,a), \phi(x,a)の計算回数が少なくなるようなaaを選択すれば効率的に計算できることが期待される。P3(x,a)P_3(x,a)まで計算するためにa=x14a = x^{\frac 14}としていることが多いようなので下記実装例ではこの値を用いている。

なお、aaを大きくすると非零なPk(x,a)P_k(x,a)の数が少なくなる一方、ϕ(x,a)\phi(x,a)は再帰的に計算するため(詳しくは後述)、aaが大きいと計算回数が多くなることから極端にaaを大きくしたり小さくしたりすれば良いわけではないようである(独自研究)。

Pk(x,a)P_k(x,a)の計算 §

前述の通り、kkは小さい値だけ計算すれば良いため、よく使われるk=2,3k=2,3の場合のみを考える。

P2(x,a)={pipjpa<pipjpipjx}P_2(x,a) = |\{p_ip_j \mid p_a \lt p_i \leq p_j \land p_ip_j \leq x\}|であり、ここで使われているpip_ipa<pixp_a \lt p_i \leq \sqrt xを満たす。ここで自然数i>ai \gt aに対して次のような関数P2(i)(x)P_2^{(i)}(x)を定義する。

P2(i)(x)={pipjpipjpipjx} P_2^{(i)}(x) = |\{p_ip_j \mid p_i \leq p_j \land p_ip_j \leq x\}|

これを用いるとP2(x,a)P_2(x,a)は次のように表すことが出来る。

P2(x,a)=i=a+1π(x)P2(i)(x) P_2(x,a) = \sum_{i=a+1}^{\pi({\sqrt x})}P_2^{(i)}(x)

pixp_i \leq \sqrt xであるから、iiとして取り得る添字はπ(x)\pi(\sqrt x)が最大であることに注意する。

ここで、pipjpipjxp_i \leq p_j \land p_ip_j \leq xを満たすpjp_jの数はπ(xpi)(i1)\pi\left({\frac{x}{p_i}}\right) - (i-1)である。これはpipjxp_ip_j \leq xを満たす素数pjp_jの数がπ(xpi)\pi\left(\frac x{p_i}\right)であり、そこからpip_i未満の素数の数であるi1i-1を引いたものになることから説明出来る。

以上より、P2(x,a)P_2(x,a)は次のようになる。

P2(x,a)=i=a+1π(x)(π(xpi)(i1))=i=a+1π(x)π(xpi)((π(x)1)π(x)2(a1)a2) P_2(x,a) = \sum_{i=a+1}^{\pi({\sqrt x})} \left(\pi\left({\frac{x}{p_i}}\right) - (i-1)\right) = \sum_{i=a+1}^{\pi({\sqrt x})}\pi\left({\frac{x}{p_i}}\right) - \left(\frac{(\pi(\sqrt x) - 1)\pi(\sqrt x)}2 - \frac{(a-1)a}2 \right)

P3(x,a)P_3(x,a)についても同様に考える。

pipjplxp_ip_jp_l \leq xとなる3つの整数pi,pj,plp_i, p_j, p_lがいずれもpap_aより大きく、pa<pipjplp_a \lt p_i \leq p_j \leq p_lとする。

この条件からpa<pix13p_a \lt p_i \leq x^{\frac 13}pjpjxpip_j \leq p_j \leq \sqrt{\frac{x}{p_i}}が成り立つ。これが満たされている上で、条件を満たすplp_lの数は、P2(i)(x,a)P_2^{(i)}(x,a)の場合と同様に考えるとπ(xpipj)(j1)\pi\left(\frac x{p_ip_j}\right) - (j-1)個となる。

以上より、P3(x,a)P_3(x,a)は次のようになる。

P3(x,a)=i=a+1π(x13)j=iπ(xpi)(π(xpipj)(j1)) P_3(x,a) = \sum_{i=a+1}^{\pi(x^{\frac 13})} \sum_{j=i}^{\pi(\sqrt{\frac {x}{p_i}})} \left(\pi\left(\frac x{p_ip_j}\right) - (j-1)\right)

P2,P3P_2, P_3のいずれにおいてもii番目の素数pip_iを計算しておく必要があるが、添字の最大がπ(x)\pi(\sqrt x)であることから、こちらもエラトステネスの篩でx\sqrt xまでの素数を列挙しておけば良い。

また、関数π\piも使われているが、小さいxxにおけるπ(x)\pi(x)は列挙した素数を二部探索することによって計算出来る。

ϕ(x,a)\phi(x,a)の計算 §

ϕ(x,a)\phi(x,a)は次のような再帰関数になる。

ϕ(x,a)=ϕ(x,a1)ϕ(xpa,a1) \phi(x,a) = \phi(x,a-1) - \phi\left(\frac x{p_a}, a-1\right)

この関数は最終的にa=1a=1の場合に辿り着くが、これはxx以下の奇数の数に相当するので計算することが出来る。

これは次のようにして導出される。

まず、δ(x,a)ϕ(x,a1)ϕ(x,a)\delta(x,a) \coloneqq \phi(x,a-1) - \phi(x,a)とおく。これはエラトステネスの篩においてpap_aで落される数を表している。よって落とされる数の集合は{pakkNpakx}\{p_ak\mid k \in \mathbb N \land p_ak \leq x\}の「部分集合」(既に落された数字を除いたもの)になる。kkの範囲についてはもう少し詳細に書くと1kxpa1 \leq k \leq \left\lfloor \frac x{p_a}\right\rfloorとなり、この中からpap_aより前の篩で落とされなかった数の数がδ(x,a)\delta(x,a)に相当する。

pap_a以前の篩で落とされた数はkkpap_a未満の素数を素因数として含んでいるものになる。よって一方の「pap_aより小さい素数で落とされなかった」数はkkがこれらを素因数として含んでいないものになり、このようなkkの総数はϕ(xpa,a1)\phi\left(\left\lfloor\frac{x}{p_a}\right\rfloor, a-1\right)となる。したがって、このようなkkと同じ数だけpap_aで落とされることになり、δ(x,a)=ϕ(xpa,a1)\delta(x,a) =\phi\left(\left\lfloor\frac{x}{p_a}\right\rfloor, a-1\right)が導かれる。

実装 §

from bisect import bisect
from math import isqrt, ceil
from typing import List
import sys
sys.setrecursionlimit(10**6)


def create_sieve(n: int) -> List[bool]:
    sieve = [True for _ in range(n + 1)]
    sieve[0] = False
    sieve[1] = False
    for i in range(2, isqrt(n) + 1):
        if not sieve[i]:
            continue

        for j in range(i**2, n+1, i):
            sieve[j] = False

    return sieve


def get_primes(n: int) -> List[int]:
    sieve = create_sieve(n)
    ret = []
    for i, res in enumerate(sieve):
        if res:
            ret.append(i)

    return ret


limit = 10**6
primes = get_primes(limit)
pi_cache = {}
phi_cache = {}

def get_prime(i: int) -> int:
    if i < 1 or i > len(primes):
        raise ValueError

    return primes[i-1]


def small_pi(x: int) -> int:
    if x in pi_cache:
        return pi_cache[x]

    if x > limit:
        raise ValueError

    i = bisect(primes, x)
    pi_cache[x] = i

    return i


def pi(x: int) -> int:
    if x in pi_cache:
        return pi_cache[x]

    if x <= limit:
        return small_pi(x)

    a = pi(int(x ** (1/4)))
    phi_xa = phi(x, a)
    p2_xa = p2(x, a)
    p3_xa = p3(x, a)

    ret = phi_xa + a - 1 - p2_xa - p3_xa

    pi_cache[x] = ret

    return ret


def phi(x: int, a: int) -> int:
    if (x, a) in phi_cache:
        return phi_cache[(x, a)]

    if a == 1:
        return (x + 1) // 2

    if x < 1:
        return 0

    ret = phi(x, a - 1) - phi(x // get_prime(a), a - 1)
    phi_cache[(x, a)] = ret

    return ret


def p2(x: int, a: int) -> int:
    ret = 0
    pi_sqrt_x = pi(isqrt(x))
    for i in range(a+1, pi_sqrt_x+1):
        ret += pi(x // get_prime(i))

    return ret - (pi_sqrt_x - 1) * pi_sqrt_x // 2 + (a - 1) * a // 2


def p3(x: int, a: int) -> int:
    ret = 0
    pi_croot_x = pi(int(x**(1/3)))

    for i in range(a+1, pi_croot_x + 1):
        p_i = get_prime(i)
        for j in range(i, pi(isqrt(x // p_i)) + 1):
            p_j = get_prime(j)
            ret += pi(x // p_i // p_j) - (j - 1)

    return ret

N = 10**11
print(pi(N))  # 4118054813

References §