APR-CL素数判定法
APR-CL素数判定法(Adleman–Pomerance–Rumely-Cohen-Lenstra primality test, APR-CL primality test)について概説&実装してみる。
この素数判定法はAdleman–Pomerance–Rumelyらによって開発された方法で、のちにCohenとLenstraによって改良されています。
数100桁程度の数に対しても現実的な時間で素数判定ができるすごいやつです。
ただし使う数学の道具も高度で、場合分けも多く、実装は重めです。確率的に「ほぼほぼ素数」と判定すればいいだけなら、Miller–Rabin法などを使う方が実装は楽です。
参考文献
- [APR] Adleman, Leonard M.; Pomerance, Carl; Rumely, Robert S. (1983). "On distinguishing prime numbers from composite numbers". Annals of Mathematics. 117 (1): 173–206.
- [Coh-Len1] Cohen, Henri; Lenstra, Hendrik W., Jr. (1984). "Primality testing and Jacobi sums". Mathematics of Computation. 42 (165): 297–330.
- [Coh-Len2] H. Cohen and A. K. Lenstra, Implementation of a new primality test. Math. Comp. 48 (1987), 103-121.
- [和田] 和田秀男, コンピュータと素因子分解, 遊星社, 1987
- [Cohen] H. Cohen, "A course in computational algebraic number theory". Springer-Verlag, Berlin, 1993
- [Chmie] A Chmielowiec, "Primality proving with Gauss and Jacobi sums", 2004.
[APR]はおそらく読まなくても大丈夫。
[Coh-Len1]が[APR]からの改良です。
[Coh-Len2]も実装の上で有用なことが書いています。
和書でAPR法の解説をしているのは[和田]しか見たことがありません。ただ実装できるレベルまでの解説は無いので、実装するなら[Cohen]の本が良いかと思います。
[Chmie]は[Cohen]を簡潔にまとめた解説論文です。
Dirichlet指標とGauss和
まずDirichlet指標とGauss和の導入をします。
乗法群から、(0でない)複素数全体の乗法群への群準同型
を素数を法とするDirichlet指標と呼びます。
つまりmod での乗算構造をそのままに、複素数の乗法に置き換えたもの。
ディリクレ指標 - Wikipedia
具体的に指標の1つを構成します。
奇素数の原始根の1つをとします。とすると
なるがで唯一に定まる。このをと表す。
イメージ的にはのような対数。
をの素因子の1つとし、でありとします。
1の原始乗根を
とすると、
とすればDirichlet指標となっています。
あるいは以下のように書いた方が直感的でわかりやすいかも。
定義から
が成立するのは明らかでしょう。
さてここで、Gauss和を
と定義します。
が成り立つため、Gauss和は
という形に表せ、この形に表したときには一意に決まることが証明できる。
よって
の意味を
と定めてもいいです。
Gauss和を用いた素数判定の原理
具体的に話を進めるために
とする。がの約数となる素数はたくさんある。例えば
であるので、などがの候補となる。
これらのについての積を
と定義する。ここではの素因子分解の中にが何回現れるかという回数である。
のとき、
となる。素数判定したい数をとすると、までの数が素数判定できる。
実装上ははテーブルにしておき、条件を満たすtを探すことになる。
はあらかじめ試し割り算で小さな素因数は無いことがわかっているものとする。つまり、としてよい。
なるを選び、さらにをの素因子の1つとし、でありとします。(最初の方のGauss和の説明と同じ記号にしています。)
このとき、が素数であれば
となる。がからまで動けば、もからまで動くので
が成り立つ。
最後の
がポイントで、もしこの合同式が成立しなければ、は合成数となる。
素数の組合せは多数あるが、が素数であれば、すべての組合せで成立しなければならず、一度でも成立しなければ合成数とわかる。
Fermatの小定理と考え方は同じ。
この合同式がすべてのについて成立しても、残念ながら素数とはいえず、次のステップに進むことになりますが、ひとまずそれは置いておきます。
この計算量を見積もってみます。は100桁ぐらいを想定しておきます。の組合せはいろいろありますが、一番大きいケースでです。
Gauss和を
と表したとき、メモリ量はのオーダーとなる。
Gauss和同士の積は、愚直に計算すると、その2乗となり程度。
の計算は繰り返し2乗法を使えるので、に比例する計算量であり、300回程度の繰り返しとなる。全体としてとなる。さらにはで計算する必要があり、多倍長計算となる。
これを多数のの組合せについて計算するので、不可能ではないレベルだが、実用上は不満のある計算量となる。
これをJacobi和を用いた形に書き換えていく。
Jacobi和への書き換え
Gauss和を用いると計算量が増えるのは、Gauss和はという大きな環に属していたことが原因といえる。
唐突だが、とをでのDirichlet指標とするとき、 Jacobi和を
と定義する。
このJacobi和はという小さな環に属しているので計算量が少ないです。先の例でいえば、が最大である。
をのとき、それ以外のときとなる自明な指標とする。
Jacobi和の性質として、以下が成り立つ。
(1) をのDirichlet指標とすると、
・
・
(2) とをなるのDirichlet指標とすると
・
例えばのとき
のようにJacobi和の積にできたりします。
新たな記号を導入します。
として、をとします。()
とし、の整数係数の和からなる群環を定義します。
と書くとき、
ととするとき、のへの作用を、
で定義します。
少しわかりにくいですが、ただの表記だけの問題で、
のように計算します。
素数判定式を以下のように言い換えます。
「とするとき、が素数であれば、1の乗根が存在し、
となる。」
ここからかなり技巧的に感じる式変形をします。
をなる整数とし、をかつなる整数の集合とするとき、
と
とすると、
と変形でき、Gauss和がJacobi和に書き換えできました。
判定する際には、をとります。ただしという制約のため、のときには場合分けが必要です。
ここから先の詳細は[Cohen]か[Coh-Len1]を参照ということで。
素数判定アルゴリズム
[Cohen]に従い、実際に実装できるレベルでアルゴリズムをまとめておきます。
事前計算
Jacobi和などを事前計算しておきます。
を素数判定したい数の上限とします。
このステップはのみにより、素数判定したい数にはよりません。
- となるを見つける。
- を割りきる素数に対し以下の(a)から(c)を行う。
(a) での原始根を見つける。
に対してとなるようなを計算する。
(b) を割りきる素数に対し、をかつなる整数とする。
Dirichlet指標を、で定義する。
(c) ・またはかつのとき、以下を計算する。
・かつのとき、を上式で計算し、以下を計算する。
素数判定
素数判定したい数を()とする。
Step1. であれば、は合成数である
Step2. に対して、
・かつであれば、
・それ以外であれば、
Step3. となる全ての素数のペアに対し
・のとき、Step 4aへ
・かつのとき、Step 4bへ
・かつのとき、Step 4cへ
・かつのとき、Step 5へ
Step 4a. を未満でと互いに素な自然数の集合とする。
・
・
・
・
・
・
このとき、となるようなの乗根が存在しないとき、は合成数である。
もしが存在し、原始冪根であるとき、とする。
Step 4b. を未満ででまたはに合同な自然数の集合とする。
・
・
・
・
・
・
ここではがかに合同であるときに、それ以外はとする。
このとき、となるようなの乗根が存在しないとき、は合成数である。
もしが存在し、原始冪根であり、かつ、のとき、とする。
Step 4c.
・
・
・のとき、
のとき、
このとき、となるようなの乗根が存在しないとき、は合成数である。
もしが存在し、原始冪根であり、かつ、のとき、とする。
Step 4d.
・
このとき、であれば、は合成数である。
もしかつのとき、とする。
Step 5. なるに対して以下を行う。
・かつとなるをランダムに選ぶ。
・Step 4a-4dをに対して行う。
・何度か繰り返してもとなるがあれば「判定不能」を返す。(現実的にはこれはまず起こらない)
Step 6.
・に対して、を計算する。
・もしあるでがを割りきれば合成数である。そうでなければ素数と判定される。
以上がAPR-CL素数判定アルゴリズムです。
かなり場合分けが煩雑ですね。
上記アルゴリズムだけでは実装には悩む箇所があるので、実装上は[Coh-Len2]を参照するといいです。
を作用させる箇所や、1のべき根であることの判定法などが具体的に記載されています。
Python3による実装
[Cohen]とはできるだけ記号を合わせているので、比較しながら読めばわかるはず。
上のアルゴリズムで事前計算とされている部分は、事前計算にせずにその場で計算しています。
原始根なども保存せずに毎回計算したりしているので、少し工夫すればもう少しは早くなるかも。
手元の環境での実行時間は、100桁だと2秒、2^521-1(157桁)が18秒、2^1279-1(386桁)が5分55秒といったところで、まずまずの速度でしょうか。
GitHub - wacchoz/APR_CL: Adleman–Pomerance–Rumely-Cohen-Lenstra primality test (APR-CL)
# -*- coding: utf-8 -*- import copy import time from math import gcd # version >= 3.5 # primality test by trial division def isprime_slow(n): if n<2: return False elif n==2 or n==3: return True elif n%2==0: return False else: i = 3 while i*i <= n: if n%i == 0: return False i+=2 return True # v_q(t): how many time is t divided by q def v(q, t): ans = 0 while(t % q == 0): ans +=1 t//= q return ans def prime_factorize(n): ret = [] p=2 while p*p <= n: if n%p==0: num = 0 while n%p==0: num+=1 n//= p ret.append((p,num)) p+= 1 if n!=1: ret.append((n,1)) return ret # calculate e(t) def e(t): s = 1 q_list = [] for q in range(2, t+2): if t % (q-1) == 0 and isprime_slow(q): s *= q ** (1+v(q,t)) q_list.append(q) return 2*s, q_list # Jacobi sum class JacobiSum(object): def __init__(self, p, k, q): self.p = p self.k = k self.q = q self.m = (p-1)*p**(k-1) self.pk = p**k self.coef = [0]*self.m # 1 def one(self): self.coef[0] = 1 for i in range(1,self.m): self.coef[i] = 0 return self # product of JacobiSum # jac : JacobiSum def mul(self, jac): m = self.m pk = self.pk j_ret=JacobiSum(self.p, self.k, self.q) for i in range(m): for j in range(m): if (i+j)% pk < m: j_ret.coef[(i+j)% pk] += self.coef[i] * jac.coef[j] else: r = (i+j) % pk - self.p ** (self.k-1) while r>=0: j_ret.coef[r] -= self.coef[i] * jac.coef[j] r-= self.p ** (self.k-1) return j_ret def __mul__(self, right): if type(right) is int: # product with integer j_ret=JacobiSum(self.p, self.k, self.q) for i in range(self.m): j_ret.coef[i] = self.coef[i] * right return j_ret else: # product with JacobiSum return self.mul(right) # power of JacobiSum(x-th power mod n) def modpow(self, x, n): j_ret=JacobiSum(self.p, self.k, self.q) j_ret.coef[0]=1 j_a = copy.deepcopy(self) while x>0: if x%2==1: j_ret = (j_ret * j_a).mod(n) j_a = j_a*j_a j_a.mod(n) x //= 2 return j_ret # applying "mod n" to coefficient of self def mod(self, n): for i in range(self.m): self.coef[i] %= n return self # operate sigma_x # verification for sigma_inv def sigma(self, x): m = self.m pk = self.pk j_ret=JacobiSum(self.p, self.k, self.q) for i in range(m): if (i*x) % pk < m: j_ret.coef[(i*x) % pk] += self.coef[i] else: r = (i*x) % pk - self.p ** (self.k-1) while r>=0: j_ret.coef[r] -= self.coef[i] r-= self.p ** (self.k-1) return j_ret # operate sigma_x^(-1) def sigma_inv(self, x): m = self.m pk = self.pk j_ret=JacobiSum(self.p, self.k, self.q) for i in range(pk): if i<m: if (i*x)%pk < m: j_ret.coef[i] += self.coef[(i*x)%pk] else: r = i - self.p ** (self.k-1) while r>=0: if (i*x)%pk < m: j_ret.coef[r] -= self.coef[(i*x)%pk] r-= self.p ** (self.k-1) return j_ret # Is self p^k-th root of unity (mod N) # if so, return h where self is zeta^h def is_root_of_unity(self, N): m = self.m p = self.p k = self.k # case of zeta^h (h<m) one = 0 for i in range(m): if self.coef[i]==1: one += 1 h = i elif self.coef[i] == 0: continue elif (self.coef[i] - (-1)) %N != 0: return False, None if one == 1: return True, h # case of zeta^h (h>=m) for i in range(m): if self.coef[i]!=0: break r = i % (p**(k-1)) for i in range(m): if i % (p**(k-1)) == r: if (self.coef[i] - (-1))%N != 0: return False, None else: if self.coef[i] !=0: return False, None return True, (p-1)*p**(k-1)+ r # find primitive root def smallest_primitive_root(q): for r in range(2, q): s = set({}) m = 1 for i in range(1, q): m = (m*r) % q s.add(m) if len(s) == q-1: return r return None # error # calculate f_q(x) def calc_f(q): g = smallest_primitive_root(q) m = {} for x in range(1,q-1): m[pow(g,x,q)] = x f = {} for x in range(1,q-1): f[x] = m[ (1-pow(g,x,q))%q ] return f # sum zeta^(a*x+b*f(x)) def calc_J_ab(p, k, q, a, b): j_ret = JacobiSum(p,k,q) f = calc_f(q) for x in range(1,q-1): pk = p**k if (a*x+b*f[x]) % pk < j_ret.m: j_ret.coef[(a*x+b*f[x]) % pk] += 1 else: r = (a*x+b*f[x]) % pk - p**(k-1) while r>=0: j_ret.coef[r] -= 1 r-= p**(k-1) return j_ret # calculate J(p,q)(p>=3 or p,q=2,2) def calc_J(p, k, q): return calc_J_ab(p, k, q, 1, 1) # calculate J_3(q)(p=2 and k>=3) def calc_J3(p, k, q): j2q = calc_J(p, k, q) j21 = calc_J_ab(p, k, q, 2, 1) j_ret = j2q * j21 return j_ret # calculate J_2(q)(p=2 and k>=3) def calc_J2(p, k, q): j31 = calc_J_ab(2, 3, q, 3, 1) j_conv = JacobiSum(p, k, q) for i in range(j31.m): j_conv.coef[i*(p**k)//8] = j31.coef[i] j_ret = j_conv * j_conv return j_ret # in case of p>=3 def APRtest_step4a(p, k, q, N): print("Step 4a. (p^k, q = {0}^{1}, {2})".format(p,k,q)) J = calc_J(p, k, q) # initialize s1=1 s1 = JacobiSum(p,k,q).one() # J^Theta for x in range(p**k): if x % p == 0: continue t = J.sigma_inv(x) t = t.modpow(x, N) s1 = s1 * t s1.mod(N) # r = N mod p^k r = N % (p**k) # s2 = s1 ^ (N/p^k) s2 = s1.modpow(N//(p**k), N) # J^alpha J_alpha = JacobiSum(p,k,q).one() for x in range(p**k): if x % p == 0: continue t = J.sigma_inv(x) t = t.modpow((r*x)//(p**k), N) J_alpha = J_alpha * t J_alpha.mod(N) # S = s2 * J_alpha S = (s2 * J_alpha).mod(N) # Is S root of unity exist, h = S.is_root_of_unity(N) if not exist: # composite! return False, None else: # possible prime if h%p!=0: l_p = 1 else: l_p = 0 return True, l_p # in case of p=2 and k>=3 def APRtest_step4b(p, k, q, N): print("Step 4b. (p^k, q = {0}^{1}, {2})".format(p,k,q)) J = calc_J3(p, k, q) # initialize s1=1 s1 = JacobiSum(p,k,q).one() # J3^Theta for x in range(p**k): if x % 8 not in [1,3]: continue t = J.sigma_inv(x) t = t.modpow(x, N) s1 = s1 * t s1.mod(N) # r = N mod p^k r = N % (p**k) # s2 = s1 ^ (N/p^k) s2 = s1.modpow(N//(p**k), N) # J3^alpha J_alpha = JacobiSum(p,k,q).one() for x in range(p**k): if x % 8 not in [1,3]: continue t = J.sigma_inv(x) t = t.modpow((r*x)//(p**k), N) J_alpha = J_alpha * t J_alpha.mod(N) # S = s2 * J_alpha * J2^delta if N%8 in [1,3]: S = (s2 * J_alpha ).mod(N) else: J2_delta = calc_J2(p,k,q) S = (s2 * J_alpha * J2_delta).mod(N) # Is S root of unity exist, h = S.is_root_of_unity(N) if not exist: # composite return False, None else: # possible prime if h%p!=0 and (pow(q,(N-1)//2,N) + 1)%N==0: l_p = 1 else: l_p = 0 return True, l_p # in case of p=2 and k=2 def APRtest_step4c(p, k, q, N): print("Step 4c. (p^k, q = {0}^{1}, {2})".format(p,k,q)) J2q = calc_J(p, k, q) # s1 = J(2,q)^2 * q (mod N) s1 = (J2q * J2q * q).mod(N) # s2 = s1 ^ (N/4) s2 = s1.modpow(N//4, N) if N%4 == 1: S = s2 elif N%4 == 3: S = (s2 * J2q * J2q).mod(N) else: print("Error") # Is S root of unity exist, h = S.is_root_of_unity(N) if not exist: # composite return False, None else: # possible prime if h%p!=0 and (pow(q,(N-1)//2,N) + 1)%N==0: l_p = 1 else: l_p = 0 return True, l_p # in case of p=2 and k=1 def APRtest_step4d(p, k, q, N): print("Step 4d. (p^k, q = {0}^{1}, {2})".format(p,k,q)) S2q = pow(-q, (N-1)//2, N) if (S2q-1)%N != 0 and (S2q+1)%N != 0: # composite return False, None else: # possible prime if (S2q + 1)%N == 0 and (N-1)%4==0: l_p=1 else: l_p=0 return True, l_p # Step 4 def APRtest_step4(p, k, q, N): if p>=3: result, l_p = APRtest_step4a(p, k, q, N) elif p==2 and k>=3: result, l_p = APRtest_step4b(p, k, q, N) elif p==2 and k==2: result, l_p = APRtest_step4c(p, k, q, N) elif p==2 and k==1: result, l_p = APRtest_step4d(p, k, q, N) else: print("error") if not result: print("Composite") return result, l_p def APRtest(N): t_list = [ 2, 12, 60, 180, 840, 1260, 1680, 2520, 5040, 15120, 55440, 110880, 720720, 1441440, 4324320, 24504480, 73513440 ] print("N=", N) if N<=3: print("input should be greater than 3") return False # Select t for t in t_list: et, q_list = e(t) if N < et*et: break else: print("t not found") return False print("t=", t) print("e(t)=", et, q_list) # Step 1 print("=== Step 1 ===") g = gcd(t*et, N) if g > 1: print("Composite") return False # Step 2 print("=== Step 2 ===") l = {} fac_t = prime_factorize(t) for p, k in fac_t: if p>=3 and pow(N, p-1, p*p)!=1: l[p] = 1 else: l[p] = 0 print("l_p=", l) # Step 3 & Step 4 print("=== Step 3&4 ===") for q in q_list: if q == 2: continue fac = prime_factorize(q-1) for p,k in fac: # Step 4 result, l_p = APRtest_step4(p, k, q, N) if not result: # composite print("Composite") return False elif l_p==1: l[p] = 1 # Step 5 print("=== Step 5 ===") print("l_p=", l) for p, value in l.items(): if value==0: # try other pair of (p,q) print("Try other (p,q). p={}".format(p)) count = 0 i = 1 found = False # try maximum 30 times while count < 30: q = p*i+1 if N%q != 0 and isprime_slow(q) and (q not in q_list): count += 1 k = v(p, q-1) # Step 4 result, l_p = APRtest_step4(p, k, q, N) if not result: # composite print("Composite") return False elif l_p == 1: found = True break i += 1 if not found: print("error in Step 5") return False # Step 6 print("=== Step 6 ===") r = 1 for t in range(t-1): r = (r*N) % et if r!=1 and r!= N and N % r == 0: print("Composite", r) return False # prime!! print("Prime!") return True if __name__ == '__main__': start_time = time.time() APRtest(2**521-1) # 157 digit, 18 sec # APRtest(2**1279-1) # 386 digit, 355 sec end_time = time.time() print(end_time - start_time, "sec")