Meissel-Lehmerのアルゴリズム
序文 §
面白いアルゴリズムを探すために、最近教えてもらったLibrary Checkerを眺めていたら素数計数関数 をまでの素数を列挙するより速く計算する方法があることが判明したので紹介します。
Algorithm §
特に断りが無ければ、は実数とする。
以下では番目の素数をとおく。1-indexedなのでである。
自然数に対して、次の関数を定義する。
これはの素因数が「全て」よりも大きいようなの個数を指している。例えばである(より、これより大きい素因数からなる10以下の数は5,7のみである、1は素数の約数を持たないことから前提が偽となり満たされる)。直感的にはエラトステネスの篩においてで篩にかけた後に残った整数の数に相当する。
また、非負整数と自然数に対して、次の関数を定義する。
これはつまり、より大きい素数をちょうど個素因数に持つ数の総数である。なお、便宜上とする。
自明な式として次がある
よって、これを変形して次が導かれる。
ここでどんなに対しても、となるが必ず存在することから、に対してある添字が存在して、それより大きい添字ではとなるため、この無限和は有限の値をとる。特にとすれば、はにおいて0になる。
よって、の計算回数が少なくなるようなを選択すれば効率的に計算できることが期待される。まで計算するためにとしていることが多いようなので下記実装例ではこの値を用いている。
なお、を大きくすると非零なの数が少なくなる一方、は再帰的に計算するため(詳しくは後述)、が大きいと計算回数が多くなることから極端にを大きくしたり小さくしたりすれば良いわけではないようである(独自研究)。
の計算 §
前述の通り、は小さい値だけ計算すれば良いため、よく使われるの場合のみを考える。
であり、ここで使われているはを満たす。ここで自然数に対して次のような関数を定義する。
これを用いるとは次のように表すことが出来る。
であるから、として取り得る添字はが最大であることに注意する。
ここで、を満たすの数はである。これはを満たす素数の数がであり、そこから未満の素数の数であるを引いたものになることから説明出来る。
以上より、は次のようになる。
についても同様に考える。
となる3つの整数がいずれもより大きく、とする。
この条件からとが成り立つ。これが満たされている上で、条件を満たすの数は、の場合と同様に考えると個となる。
以上より、は次のようになる。
のいずれにおいても番目の素数を計算しておく必要があるが、添字の最大がであることから、こちらもエラトステネスの篩でまでの素数を列挙しておけば良い。
また、関数も使われているが、小さいにおけるは列挙した素数を二部探索することによって計算出来る。
の計算 §
は次のような再帰関数になる。
この関数は最終的にの場合に辿り着くが、これは以下の奇数の数に相当するので計算することが出来る。
これは次のようにして導出される。
まず、とおく。これはエラトステネスの篩においてで落される数を表している。よって落とされる数の集合はの「部分集合」(既に落された数字を除いたもの)になる。の範囲についてはもう少し詳細に書くととなり、この中からより前の篩で落とされなかった数の数がに相当する。
以前の篩で落とされた数はが未満の素数を素因数として含んでいるものになる。よって一方の「より小さい素数で落とされなかった」数はがこれらを素因数として含んでいないものになり、このようなの総数はとなる。したがって、このようなと同じ数だけで落とされることになり、が導かれる。
実装 §
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 §
- Meissel–Lehmer algorithm - Wikipedia
- acganesh - Efficient Prime Counting with the Meissel-Lehmer Algorithm: 元サイトがFirefoxに警告吐かれたのでInternet Archive