http://blog.practical-scheme.net/shiro/20130216a-factorizeを読んで疑問に思ったので調べた。
敢えて実装済みのMonte Carlo Factorizationで挑戦してみよう。
gosh> (use math.prime) #<undef> gosh> (time (mc-factorize 280671392065546467397265294532969672241810318954163887187279320454220348884327)) ;(time (mc-factorize 280671392065546467397265294532969672241810318954163 ... ; real 552.461 ; user 552.310 ; sys 0.110 (162425297 215940091 358456949 369941863 369941863 479871607 706170617 481362815814826159)9分ちょいかかった。
試しにGauche 0.9.4 pre3を手元で動かしてみるともっと時間がかかった。
gosh> (time (mc-factorize 280671392065546467397265294532969672241810318954163887187279320454220348884327)) ;(time (mc-factorize 280671392065546467397265294532969672241810318954163 ... ; real 884.309 ; user 883.320 ; sys 0.050
で本題は次の部分。
GNFSでこれが1秒で計算できるなら、 Monte Carlo FactorizationをスキップしてGNFS実装した方が良かったかなあ。
GNFS(一般数体ふるい法)は少し解説を読んだだけでもかなり実装がたいへんそうなアルゴリズムなので、実際にGNFSの実装をうたっているmsieveはともかくPerlやPythonのそれほど専門向けではなさそうなライブラリでGNFSを使っているのだろうかと疑問に感じた。それで調べてみるとやはりGNFSを使っているわけではないようだ。
どうやらPythonのSymPyはポラードのρ法とp-1法を使っていて、PerlのMath::Prime::Utilはそれに加えて(特にGNU Multi-Precision Library使用版で)いくつかのアルゴリズムを併用しているみたい。
他の実装との速度比較
上と同じ数について因数分解の時間を計測してみると次のようになった。
Python factorint 1.70(sec.) Python factorint (only rho method) 4.91 Python factorint (only p-1 method) 13.32 Perl factor (GMP) 0.19 Perl factor (Pure Perl) 236.28
こうしてみるとアルゴリズム上あまり違いがないと思われるPythonのρ法版と比べてGaucheの結果が非常に悪い。しかしGaucheがそんなに遅いとは思えない。(ABC予想についてのデータを作ったときも十分速かった印象がある)
ということはアルゴリズムの実装のどこかにバグがありそう。
ポラードのρ法
Gaucheはρ法(モンテカルロ法)のブレントによる変形版を実装しているのだけど、ポラートによるオリジナルのアルゴリズムがかなり単純なのでそっちをWikipediaを見ながらコードに移した。
;;; Gaucheの実装でmc-find-divisor-1と呼ばれている部分を置き換え。 (define (mc-find-divisor-1 n x0) (pollard-rho n x0)) (define (pollard-rho n x0) (define (f x) (remainder (+ (* x x) 2) n)) (let loop ((U x0) (V x0)) (let ((U (f U)) (V (f (f V)))) (let1 g (gcd (- U V) n) (cond ((= g 1) (loop U V)) ((= g n) #f) (else g))))))
実行してみる。
gosh> (time (mc-factorize 280671392065546467397265294532969672241810318954163887187279320454220348884327)) ;(time (mc-factorize 280671392065546467397265294532969672241810318954163 ... ; real 3.183 ; user 3.160 ; sys 0.000 (162425297 215940091 358456949 369941863 369941863 479871607 706170617 481362815814826159)
いっきに速くなってPythonと比べても遜色のない速度になった。やはり元の実装のどこかに問題があったみたい。ただどこに問題があるのかよく分からない。いかにも手続き的な感じの多重ループは(代入を避けると)Schemeのコードに移しづらいので、元のアルゴリズムとSchemeのコードをうまく比較できなかった。
ブレントのアルゴリズム
しょうがないのでブレントのアルゴリズムを半ば機械的にSchemeのコードに移して、同じ因数分解を実行してみた。
(time (mc-factorize 280671392065546467397265294532969672241810318954163887187279320454220348884327)) ;(time (mc-factorize 280671392065546467397265294532969672241810318954163 ... ; real 0.729 ; user 0.730 ; sys 0.000 (162425297 215940091 358456949 369941863 369941863 479871607 706170617 481362815814826159)
さらに速くなった。何か重要な処理を飛ばしているために速くなっている可能性もある(変形版の方が遅い?みたいな文章もあった)。それにρ法の実行速度は入力した数の大きさではなく素因数の大きさで決まるようなのでこれだけ見ても性能はよくわからない。素因数の大きさに依存しないアルゴリズムを使っても80桁ぐらいの数で数時間くらいかかるみたい(Appendix 1. Factorization resultsなど)。
けれど、取りあえずよいことにする。終わり。
(追記: 『UBASICによるコンピュータ整数論』(電子版)P.163の
問題 13 rho1 ではGCDを毎回計算している。これは大変時間のかかる計算なので普段はA1-A2[上のプログラムでの(- U V)]の積を作っておいて100回に1回だけGCDを計算するようにプログラムを改造せよ。
に従ってポラードのρ法のプログラムを書き換えたらいくらか速くなった。
(define (mc-find-divisor-1 n x0) (pollard-rho2 n x0)) (define (pollard-rho2 n x0) (define (f x) (remainder (+ (* x x) 2) n)) (define N-loop 100) (let loop ((U x0) (V x0)) (receive (U V W) (let inner-loop ((i 0) (U U) (V V) (W 1)) (if (> i N-loop) (values U V W) (let ((U (f U)) (V (f (f V)))) (inner-loop (+ i 1) U V (* W (remainder (- U V) n)))))) (let1 g (gcd W n) (cond ((= g 1) (loop U V)) ((= g n) #f) (else g))))))
追記終わり)
追記: ポラードのρ法の考え方
こういう場合、彼は思いがけぬ偶然にたよったのである。合理的な連脈の糸をたどれぬこのような場合には、彼は冷静に、そして慎重に不合理の連脈に従った。(…)なにか一つでも手がかりがある場合なら、こんな方法は下の下だが、ぜんぜん手がかりがない場合には、これこそ最善の方策だ。
(G・K・チェスタトン「青い十字架」(中村保男訳))
合成数の因数(素因数とは限らない)を見つけたいとする。
ポラードのρ法は簡単に言えば次のようなアルゴリズム。
このアルゴリズムは、次の性質に基づいている。
nの素因数のひとつをpとする。となるとについて
ならば、となる。このときはの因数である(ただしとが等しい場合はとなる)。
例えばn=299として、ランダムにaとbを選んで、a=208,b=52を選んだとする。
このとき偶然にも、nのある素因数pについてが成り立っている。そのため
となり、299の因数(この場合、素因数)13が得られる。
このような偶然がどれくらいの確率で起こるかを考えてみる。
とをデタラメに選んでみる。このときとなる確率は。したがってとだいたい同じ程度の確率でとなる。(ここでは最小の素因数)。
そしてのとき因数が見つかったことになる(ただしその見つかった因数がである可能性もある。確率はぐらい)。
しかしとを毎回ランダムに選ぶととなるまでに平均回程度の繰り返しが必要になってしまい意味がない(それなら普通に小さい順にを試し割りしていけばいい)。
もっと少ない回数でとなるものを見つけたい。
そのために次のようにする。
の値を取り乱数っぽく振る舞う関数を用意する。この関数はならばとなる性質を持っている必要がある(この性質はが「多項式(mod n)」の形のときは満たされている)。そして適当な初期値から始めて
という疑似乱数列を作っていく。この系列の中からとなるものを見つける。の余りは通りしかないので、いずれどこかで(系列の番目と番目)で
となるものが出てくる。このときの性質から、ここから先は
というように常に一致する。つまりで考えると、番目から系列の循環が始まる(ロー法という名前はこの循環にいたる様子を「ρ」の形に見立てている)。
そして系列が循環していることはいわゆるウサギとカメのアルゴリズム(フロイドの循環検出法 - Wikipedia)で検出できる。
ふたつの系列を計算していき
となるかを見ていれば循環が検出できの因数が見つかることになる。
ρ法にかかる時間の見積り
ここで問題になるのは同じ値が登場するまでにどれくらいかかるか。最悪の場合(でのすべての値を網羅する場合)回かかってしまう。
もし値をランダムに選んでいったとするといわゆる「誕生日問題」になり、同じ値が出るまでの平均回数はおおよそに比例した値になる。そのため系列をランダムだとみなせば循環までにかかる回数はに比例すると見積もれる(ただしが線形合同法だと循環の長さがに比例してしまうのでρ法と相性が悪い)。
実行にかかる時間はよりも素因数の大きさに支配されている。もちろんで割った余りやとの最大公約数を取る処理があるのでにも依存しているけれど。そのため比較的小さな素因数を持っている場合は高速に実行できると予想される。うまくいかないのは次のとき。
- 素因数がどれも大きいために循環するまでにかかる時間が大きくなる場合(→他のアルゴリズムを使うかあきらめる)。
- 循環までにかかる時間が期待値よりもずっと大きくなる場合(単に運が悪かった? や初期値との相性が悪かった?)。
- 見つかった因数が元の数と等しかった場合(がランダムなら非常に低確率。との相性が悪いと発生しやすいかもしれない?)。
ブレントのアルゴリズムのコード
使ったブレントのアルゴリズムのコードは以下のもの。
(define (mc-find-divisor-1 n x0) (brent n x0)) (define (brent n x0) (define m (ceiling->exact (log n 2))) (define (f x) (remainder (+ (* x x) 2) n)) (define (f^ k x) (let loop ((k k) (x x)) (if (= k 1) (f x) (loop (- k 1) (f x))))) (define (first-step) (outer-loop)) (define (outer-loop) (let loop ((y x0) (r 1) (q 1)) (let* ((x y) (y (f^ r y))) (receive (g y ys q) (middle-loop x y r q) (let1 r (* r 2) (if (> g 1) (second-step g x ys) (loop y r q))))))) (define (middle-loop x y r q) (let loop ((k 0) (y y)) (let1 ys y (receive (y q) (inner-loop (min m (- r k)) x y q) (let ((g (gcd q n)) (k (+ k m))) (if (or (>= k r) (> g 1)) (values g y ys q) (loop k y))))))) (define (inner-loop ntimes x y q) (let loop ((i 1) (y y) (q q)) (if (> i ntimes) (values y q) (let* ((y (f y)) (q (remainder (* q (abs (- x y))) n))) ; absは不要のはず (loop (+ i 1) y q))))) (define (second-step g x ys) (cond ((not (= g n)) g) (let loop ((ys (f ys))) (let1 g (gcd (abs (- x ys)) n) ; absは不要のはず (if (> g 1) (if (= g n) #f g) (loop (f ys))))))) (first-step))