素因数分解アルゴリズム(特にSQUFOF)のこと

  1. 主要な素因数分解アルゴリズム
  2. SQUFOFについて

目次

主要な素因数分解アルゴリズム

ここでいう素因数分解アルゴリズムは完全に素因数分解をするアルゴリズムではなく因数(約数)をひとつ見つけ出すアルゴリズムなので、完全な分解が必要なら再帰的に実行したり試し割り法と組み合わせる必要がある。

素因数の性質に依存するアルゴリズム(ρ法、p-1法、楕円曲線法)

都合のよい性質を持った素因数を含んでいる場合に成功するアルゴリズム。都合のよい素因数を含んでいる場合は、汎用のアルゴリズムよりも早く素因数を見つけ出したり大きな素因数を取り出したりすることができる。

  • ρ法
  • p-1法
    • 素因数pが「p-1は小さな素因数しか持っていない」という性質を持っている場合にうまくいく。「小さな素因数」というのがどれくらいの範囲かは補助的に与える数によって決まる(見つけられる素因数の範囲を広くするためには補助的に与える数を大きくしておかないといけない)。
  • 楕円曲線
素因数の性質に依存しないアルゴリズム(SQUFOF、二次ふるい法、数体ふるい法)

SQUFOFについて

SQUFOF(Square Form Factorization)について書かれた日本語の文章がほとんどないようなので説明を書いておく(SQUFOFの日本語名も特に存在しないみたい。平方形式分解?)。

連分数をもちいた素因数分解

SQUFOFはM. A. Morrison, J. Brillhart A Method of Factoring and the Factorization of F7(1975)で使われたアルゴリズム(連分数を利用した素因数分解アルゴリズムの一種)から派生したものなので、まずはそちらを少し説明する。
ρ法は疑似乱数列の中から「x\equiv y \bmod p」となるものを(素因数pを知らずに)見つけるという手法だった。
モリソンとブリルハートは「x^2 \equiv y^2 \bmod N」となるもの見つけようとする。もし自明でないx,yがうまく見つかると\gcd(x\pm y, N)によってNの因数が得られる(これは二次ふるい法や数体ふるい法でも使われる考え方)。
それを見つけるために連分数展開を利用する。
因数分解したい数Nについて\sqrt{N}を連分数展開していく。

 \sqrt{N}  =  q_0 +\large\frac{1}{\large{q_1 + \large\frac{1}{  \ddots + \frac{\vdots }{\large{q_{i-1}+   \frac{1}{{\large\frac{\sqrt{N}+P_i}{Q_i}}}} }}}} 

すると次の漸化式が成り立っている。

\begin{array}{lll} & Q_0=1, & q_0= \lfloor \sqrt{N} \rfloor \\  P_1 = q_0, & Q_1=N-q_0^2, & \\ & & q_{i} = \left\lfloor \frac{q_0 + P_i}{Q_i} \right\rfloor \\  P_{i+1}= q_i Q_i - P_i, \quad & Q_{i+1} = Q_{i-1} - q_i \left(P_i - P_{i+1} \right) \quad &  \end{array}

また連分数展開を途中で打ちきった近似連分数

\large\frac{\large{A_i}}{\large{B_i}}  =  q_0 +\large\frac{1}{\large{q_1 + \large\frac{1}{  \ddots + \frac{\vdots }{\large{q_{i-1}} }}}} 

を考えると、A_i,B_iも漸化式で表すことができる(この漸化式はSQUFOFでは使わないので省略する。連分数についての多くの解説に書いてある)。
このとき

A_i^2 - B_i^2 N = (-1)^i Q_i 

が成り立つ。{}\bmod Nで考えると

A_i^2 \equiv (-1)^i Q_i \bmod N

となり、等式の左辺が二乗の形になっている。そこでいくつかのiについて(-1)^i Q_i \bmod Nを集めてそれをかけ合わせて全ての素因数の指数が偶数になるようにできれば右辺も二乗の形になり「x^2 \equiv y^2 \bmod N」となるx,yが得られる。
これがモリソンとブリルハートのアルゴリズムの概要。

シャンクスの改良

モリソンとブリルハートのアルゴリズムは次のようして単純化することができる。

  • \sqrt{N}の連分数展開を続けていってiが偶数のときでQ_iが完全平方になるものが現れるまで待てば、Q_iをいくつも集めずに平方となる解を得ることができる。

ただしこの場合、ある程度の長さ展開を続けていかないといけない。連分数の近似分数A_i/B_iの分子分母は展開が進むにつれどんどん増大していくので、A_iの増大を抑えるために{}\bmod Nで余りを取り続けないといけない。
SQUFOFを考案したシャンクス(D. Shanks)は連分数の展開過程を二次形式の変換に対応させることで、A_iを全く使うことなく因数を計算する方法を考案した(その理屈は省略する)。

アルゴリズムの説明

(おおよそJ. E. Gower, Samuel S. Wagstaff Jr. Square Form Factorizationの§3.1、3.3、3.5、5.2に基づく)
前提条件: 分解したい数N>0は、偶数でない、素数でない、完全平方でないこと(またNが小さいと連分数展開の周期が短いので失敗する可能性が大きくなる)。
※ [Gower&Wagstaff]では理論的な取扱いのためN \equiv 1 \pmod 4ときはN2Nに取り替えるという操作をしているが、この操作をしなくてもアルゴリズムは動作し多くの実装でもこの操作をしていないとある(§3.1)。
初期値と漸化式:

\begin{array}{lll} & Q_0=1, & q_0= \lfloor \sqrt{N} \rfloor \\  P_1 = q_0, & Q_1=N-q_0^2, & \\ & & q_{i} = \left\lfloor \frac{q_0 + P_i}{Q_i} \right\rfloor \\  P_{i+1}= q_i Q_i - P_i, \quad & Q_{i+1} = Q_{i-1} - q_i \left(P_i - P_{i+1} \right) \quad &  \end{array}
  1. 前進過程: 連分数展開の漸化式を使って展開を続けていき、iが偶数でQ_iが完全平方になるものを見つける。
    • ただし自明な解になってしまいそうな場合はそのQ_iは選ばず展開を続行する。次のように判定する。
      • 見つけた\sqrt{Q_i}がそれ以前の展開のどこかに現れたQ_i(Q_iが偶数ならQ_i/2)と等しい場合は選択せず展開に戻る(展開に出てくるQ_iはある値より小さいものだけを記憶しておけばいい。後述)。
    • 展開のステップ数がある上限Bを越えた場合は失敗とする。また記憶した値の数がある程度増加したら(50個ぐらい?)失敗にする。
  2. 後退過程: 前進過程の最後に得た項\frac{\large{\sqrt{N}+P_i}}{\large{Q_i}}\frac{\large{\sqrt{N}+(-P_i)}}{\large{\sqrt{Q_i}}}に置き換えて、さらに展開を続ける。
    • Q_iの漸化式は二つ前の値Q_{i-2}も使っているので、初期値の設定にはQ_i = \frac{N-P_i^2}{Q_{i-1}}の関係を使う。前進過程の最後に得た項をP_i,Q_iとして次のようになる。
      •  Q'_0 = \sqrt{Q_i}
      •  P'_1 = P_i + \sqrt{Q_i} \left\lfloor \frac{\large{q_0 - P_i}}{\large{\sqrt{Q_i}}} \right\rfloor
      •  Q'_1 = \frac{\large{N - {P'_1}^2} }{ \large{\sqrt{Q_i}} }
    • 再び展開をおこない、P_iと次の項P_{i+1}が等しくなったところで展開を止める(後退過程での展開のステップ数は前進過程のおよそ半分ぐらい)。 
      • Q_iが偶数なら\frac{Q_i}{2}を見つかった因数とする。
      • Q_iが奇数ならQ_iを見つかった因数とする。

前進過程でQ_iを記憶する部分は次のようにする。

  1. L=\left\lfloor  \sqrt{2\sqrt{N}} \right\rfloor閾値とする。
    • Q_iが偶数ならば、 \frac{Q_i}{2} \leq Lの場合に\frac{Q_i}{2}を記録する。
    • Q_iが奇数ならば、Q_i \leq Lの場合にQ_iを記録する。
    • (小さいQ_iは非常に少ないので、まず Q_i \leq 2Lかどうかをチェックして他の判定は後にするのが良い)
  2. 完全平方のQ_iが見つかったとき、\sqrt{Q_i}がそれまでに記憶したどの値とも違っていれば後退過程に進む。
  3. 展開ステップの上限回数BB=4L

N=63375401385616362433としてみる。L=\left\lfloor  \sqrt{2\sqrt{N}} \right\rfloor=126181

  1. Q0=1、P1=7960866874、Q1=65830557
  2. (記録) i=21127の時、Qi=223456≦2Lなので、223456/2=111728を記録。
  3. (完全平方) i=42462(偶数)の時、Qi=12483145984は完全平方。√Qi=111728は記録されているので展開を続ける。
  4. (記録) i=45890の時、Qi=114549≦Lなので、114549を記録。
  5. (記録) i=58124の時、Qi=1803≦Lなので、1803を記録。
  6. (完全平方) i=59398(偶数)の時、Qi=634082761は完全平方。√Qi=25181は記録されていないので後退過程に進む。(このときPi=7554780076)
  7. 展開の初期値はQ0=25181、P1=7960848882、Q1=11376179489になる。
  8. i=29807でPi=Pi+1=7692565499。このときQi=7692565499は奇数なので、7692565499を答えとする。

63375401385616362433 / 7692565499 = 8238526067

アルゴリズムの改良

Nを展開する代わりに小さな奇数mを取ってmNを展開する: あるmで探索が失敗しても別のmを試すことができる。m次第で少ない展開数でQ_iを見つけられるかもしれない。ただし自明な因数mを除くようにアルゴリズムを少し変更する。

  • 変更点1:Q_iを記録するところ:  \frac{Q_i}{\gcd(Q_i, 2m)} \leq Lならば、 \frac{Q_i}{\gcd(Q_i, 2m)}を記録する。
  • 変更点2:P_i=P_{i+1}となったところ: \frac{Q_i}{\gcd(Q_i, 2m)}を見つかった因数とする。
  • 性能の良いmは3,5,7,11およびそれらからいくつかを選んで(全部もあり)かけ合わせたもの。

複数のmmNを展開していく: 例えば「何百ステップか展開するごとに別のmでの展開に移る」というようにして複数のmNを同時展開していくと、どれかの系列で早くQ_iを見つけられてトータルで実行時間を減らせる(かもしれない)。
完全平方なQ_iの選択条件の変更: Q_iの記憶と比較の処理を上で説明したものよりも複雑なものにすると、適切なQ_iの発見までの展開数を減らすことができる(上のやり方だと適切なQ_iを飛ばす可能性がある)。ただし処理時間の点で(どれくらい)有利かは不明。
計算の省力化(アルゴリズム自体の改良ではない): 計算に時間がかかる部分はq_iを求めるときの除算とQ_iが完全平方かを判定するところ。q_iq_i=1となることが多い(連分数展開しているのでq_i=0になることはない)ので、はじめにq_i=1かどうかを除算なしで判定するというやり方が考えられる。また完全平方かどうかはルートを取ってみないでも判定できる場合がある。

 \begin{eqnarray} (\large{\cdots001\overbrace{0\cdots0}^{n}})^2 & = & \cdots 001\overbrace{0\cdots\cdots0}^{2n} \\ (\large{\cdots011\overbrace{0\cdots0}^{n}})^2 & = & \cdots 001\overbrace{0\cdots\cdots0}^{2n} \\ (\large{\cdots101\overbrace{0\cdots0}^{n}})^2 & = & \cdots 001\overbrace{0\cdots\cdots0}^{2n} \\ (\large{\cdots111\overbrace{0\cdots0}^{n}})^2 & = & \cdots 001\overbrace{0\cdots\cdots0}^{2n} \end{eqnarray}

二乗したものを二進数で見ると末尾が必ず001\overbrace{0\cdots\cdots0}^{2n}となる。そのためまずこの形かどうかを調べると判定の効率がよくなるかもしれない。

Schemeによるプログラム例
;; Gauche以外では以下の定義を必要に応じて追加。
;; SRFI-8
(define-syntax receive
  (syntax-rules ()
    ((receive formals expression body ...)
     (call-with-values (lambda () expression)
                       (lambda formals body ...)))))
;; R6RS
(define (exact-integer-sqrt x)
  (let ((sqrt-x (inexact->exact (floor (sqrt x)))))
    (values sqrt-x (- x (* sqrt-x sqrt-x)))))

;; Gauche固有
(define (floor->exact x)
  (inexact->exact (floor x)))

;; Gauche固有
(define-syntax let1
  (syntax-rules ()
    ((_ var expr expr0 ...)
     (let ((var expr)) expr0 ...))))
;; 入力値のチェックや記録したQiの個数が多くなったときの処理は省略した
;; n>0 nは偶数、素数、完全平方ではないこと
;; 乗数multは小さな奇数 1, 3, 5, 7, 11, 3*5, 3*7, etc.
(define (squfof n mult)
  (let* ((nm (* n mult))
         (L (floor->exact (sqrt (* 2 (sqrt nm)))))
         (m*2 (* 2 mult))
         (L*m*2 (* m*2 L))
         (max-steps (* 4 L)))
    (receive (q0 nm-q0*q0) (exact-integer-sqrt nm)
      ;; 以上で初期パラメータ計算終了

      ;; PiとQiの漸化式
      (define (next-terms Qi-prev Pi Qi)
        (let* ((qi (quotient (+ q0 Pi) Qi))
               (Pi-next (- (* qi Qi) Pi))
               (Qi-next (+ Qi-prev (* qi (- Pi Pi-next)))))
          (values Pi-next Qi-next)))

      ;; Qi/gcd(Qi, 2m) <= Lの時、Qi/gcd(Qi, 2m)を記録する
      (define (update-saved-Qis saved-Qis Qi)
        (if (not (<= Qi L*m*2))
          saved-Qis
          (let1 tmp (/ Qi (gcd Qi m*2))
            (if (<= tmp L)
              (cons tmp saved-Qis)
              saved-Qis))))

      ;; 前進過程
      (define (forward-cycle i Qi-prev Pi Qi saved-Qis)
        (if (> i max-steps)
          #f
          (let1 saved-Qis (update-saved-Qis saved-Qis Qi)
            (receive (Pi-next Qi-next) (next-terms Qi-prev Pi Qi)
              (if (even? i)  ; Q_{i+1}を調べるので偶奇判定が逆になる
                (forward-cycle (+ i 1) Qi Pi-next Qi-next saved-Qis)
                (receive (sqrtQ rem) (exact-integer-sqrt Qi-next)
                  (if (or (not (zero? rem))
                          (memv sqrtQ saved-Qis))
                    (forward-cycle
                      (+ i 1) Qi Pi-next Qi-next saved-Qis)
                    (backward-cycle Pi-next sqrtQ))))))))

      ;; 後退過程
      (define (backward-cycle P-last sqrtQ)
        (let1 Pi-tmp (+ P-last
                        (* sqrtQ (floor->exact (/ (- q0 P-last) sqrtQ))))
          (let loop ((Qi-prev sqrtQ)
                     (Pi Pi-tmp)
                     (Qi (/ (- nm (* Pi-tmp Pi-tmp)) sqrtQ)))
            (receive (Pi-next Qi-next) (next-terms Qi-prev Pi Qi)
              (if (not (= Pi Pi-next))
                (loop Qi Pi-next Qi-next)
                (/ Qi (gcd Qi m*2)))))))

      (forward-cycle 1 1 q0 nm-q0*q0 '()))))