1
0

簡単な素数判定法

Posted at

この記事は2008年に発表されました。

オリジナル記事を読み、私のニュースレターを購読するには、ここ でご覧ください。


SICPのセクション1.3には素数を判定する方法が記載されています。学生たちに広く知られている単純な素数判定法は、nの最小の約数が1以外で自分自身であれば、nは素数であるというものです。

(define (remainder num divisor)
    (- num (* divisor (round (/ num divisor)))))

; nの最小の約数を返す
(define (smallest-divisor n) 
    ; nの約数(2からnまで)を探す
    (define (find-divisor n test-divisor)
        (cond ((< n (sqrt test-divisor)) n)
            ((= (remainder n test-divisor) 0) test-divisor)
            (else (find-divisor n (+ test-divisor 1)))))

    (find-divisor n 2))

; nが素数かどうかをチェックする
(define (prime? n)
    (= n (smallest-divisor n )))

しかし、この方法は遅く、複雑度はO(n)です。

SICPでは次にフェルマーの小定理が紹介されています:

もしnが素数であり、a<nの任意のaについて、a^n≡a(mod n)である。

残念ながら、逆定理は成り立ちませんが、それでも有用です。nより小さい数aを選び、上記のテストを行います。もしa^n≡a(mod n)が成り立つなら、nは非常に高い確率で素数です。より多くのaをnに対してテストするほど、nが素数である可能性が高くなります。したがって、これは確率的なアルゴリズムであり、十分なテスト回数を行えば、nが素数でない確率は非常に小さくなります。

これにより、フェルマーのテストプログラムが導かれます。a^nの計算効率を心配する必要はありません:

考えてみましょう:

a^n = (a^(n/2))^2; nが偶数の場合
a^n = a * a^(n-1); nが奇数の場合

これは常に成り立つので、ほぼすべての乗算でnが2倍になります。数直線を見てみると、これは基本的に二分探索です。したがって、時間計算量が減少します。これにより、プログラミング言語が提供するpow関数が自分でa*a*a…と書くよりも常に速い理由が説明されます。

しかし、直接組み込みのpow関数を使用すべきではありません。理由は、我々の目標はaのn乗ではなく、aのn乗をmで割った余りがaの余りに等しいかどうかを確認することだからです。簡単に言えば、この式が成り立つかどうかを確認するだけです:a^n mod m = a。もしpow(a, n) % m == aと直接表現すると、pow(a, n)の値が非常に大きくなる可能性があります。したがって、同様の技法を使用する必要があります:

a^n mod m = ((a^(n / 2))^2) mod m; nが偶数の場合
a^n mod m = (a * a^(n-1)) mod m; nが奇数の場合

この方法では、計算された値はmを超えることはなく、非常に大きな数値での計算を避けることができます。したがって、次のプログラムが簡単に得られます:

(define (remainder num divisor)
    (- num (* divisor (round (/ num divisor)))))

(define (expmod base exps m)
    (define (square n) (* n n))
    (cond ((= exps 0) 1)
        ((even? exps)
            (remainder (square (expmod base (/ exps 2) m))
                m))
        (else
            (remainder (* base (expmod base (- exps 1) m))
                m))))

; フェルマーのテスト、nをチェックする。もし(a^n) mod n == aなら、ほぼ間違いなくnは素数
(define (fermat-test n)
    (define (try-it a)
        (= a (expmod a n n)))
        ;(= a (remainder (expt a n) n)))
    (try-it (+ 1 (random (- n 1)))))

; フェルマーのテストで素数を取得する。nが素数かどうかをチェックする。試行回数timesを試す
(define (fermat-prime? n times)
    (cond ((= 0 times) #t)
        ((fermat-test n) (fermat-prime? n (- times 1)))
        (else #f)))

しかし、完全ではありません。

フェルマーのテストを欺くことができるカーマイケル数という種類の数が存在します。2から10000の間に、いくつかのこのような数があります:

561, 1105, 1729, 2465, 2821, 6601, 8911

これらの頑固な数は合成数ですが、どのaを使ってもフェルマーのテストにおいて素数のように振る舞うことができます。彼らは数が少ないわけではなく、広く分布しており、100,000,000の範囲内に255のカーマイケル数があります。したがって、カーマイケル数のリストを維持したくない場合、素数判定方法を強化することができます。

幸運なことに、エクササイズ1.28でミラーとラビンがミラー・ラビンテスト方法をもたらしました。この方法はフェルマーの小定理の変形に基づいています:

nが素数なら、a < nの任意のaについて、a^(n-1) ≡ 1(mod n)

MRテストを使用するには、フェルマーのテストの平方ステップで「非自明な平方根1 mod n」を確認する必要があります。もしs = a^2; とs <> 1およびs <> n-1が存在し、s^2 mod m = 1なら、nは素数ではありません。

Matrix67:ミラー・ラビン素数判定法も確率的アルゴリズムであり、基数aに対するミラー・ラビンテストを通過する合成数を基数aに対する強擬素数と呼びます。基数2に対する最初の強擬素数は2047です。基数2および3に対する最初の強擬素数は1,373,653のように大きいです。

したがって、十分なテスト回数があれば、この方法は工業用途において安全性を保証できます。以下は私の質問1.28に対する答えです:

(define (remainder num divisor)
    (- num (* divisor (round (/ num divisor)))))
    
(define (mr-expmod base exps m)
    (define (square x) (* x x))
    (define (check-square-root x n)
        (define (check r)
            (if (and (not (= x 1)) (not (= x (- n 1))) (= r 1)) 0 r))
        (check (remainder (square x) n)))

    (cond ((= exps 0) 1)
        ((even? exps)
            (check-square-root(mr-expmod base (/ exps 2) m) m))
        (else
            (remainder (* base (mr-expmod base (- exps 1) m))
                m))))

; ミラー・ラビンテスト、nをチェックする。もし(a^(n-1)) mod n == 1なら、ほぼ間違いなくnは素数
(define (mr-test n)
    (define (try-it a)
        (= 1 (mr-expmod a (- n 1) n)))
    (try-it (+ 1 (random (- n 1)))))

; ミラー・ラビンテストで素数を取得する。nが素数かどうかをチェックする。試行回数timesを試す
(define (mr-prime? n times)
    (cond ((= 0 times) #t)
        ((mr-test n) (mr-prime? n (- times 1)))


        (else #f)))

いくつかのデータをまとめ、[1000, 1100]、[10000, 10100]、[100000, 100100]、および[1000000, 1000100]の範囲でこれらの方法がどのように機能するかを見てみました:

    従来の方法        従来の方法の約数2の最適化     ミラー・ラビンテスト方法
    [1000, 1100]                 1 ms                       1 ms                            1 ms
    [10000, 10100]               13 ms                      8 ms                            1 ms
    [100000, 100100]             142 ms                     78 ms                           1 ms
    [1000000, 1000100]           1337 ms                    830 ms                          1 ms

これは私のIntel Core 1 1.83GHzマシン(Ubuntu 8.04 + DrScheme)で実行されました。

また、Cバージョンを書き、以下の結果が得られました:

範囲 [2, 10000]:

    $ ./prime
    Normal:  return 0 cost 10ms
    Fermat: return 0 cost 24ms
    Miller-Rabin: return 0 cost 28ms

範囲 [200000, 1000000]:

    $ ./prime
    Normal: return 0 cost 3088ms
    Fermat: return 0 cost 824ms
    Miller-Rabin: return 0 cost 959ms

gprof分析結果は概ね予想通りであり、レポートの一部を以下に示します:

    各サンプルは0.01秒としてカウントされます。
    %   累積時間   自己時間          自己呼び出し   合計
    時間   秒        秒       呼び出し  us/呼び出し  us/呼び出し  名前
    63.55      2.58     2.58   800000     3.23     3.23  is_prime(int)


    15.76      3.22     0.64   800005     0.80     0.84  fermat_expmod(long, long, long)
    10.10      3.63     0.41   800012     0.51     0.88  mr_expmod(long, long, long)
    7.27      3.92     0.29 14813786     0.02     0.02  chk_unusual_square_root(long, long)
    1.23      3.98     0.05                             main
    0.86      4.01     0.04   800000     0.04     0.93  mr_test(long, long)
    0.74      4.04     0.03 14813658     0.00     0.00  square(long)
    0.25      4.05     0.01   800000     0.01     0.94  mr_is_prime(int)
    0.25      4.06     0.01   800000     0.01     0.85  fermat_is_prime(int)
    0.00      4.06     0.00   800000     0.00     0.84  fermat_test(long, long)

小さなn値でのパフォーマンスが低い理由は、Cでの通常の再帰処理のオーバーヘッドによるものであると思います。しかし、データサイズが増えると、このオーバーヘッドは比較的無視できるものになります。誰かが上記のアルゴリズムを反復形式(私にはできません :P)や末尾再帰形式(gccは末尾再帰を反復形式に最適化できます)に変換できれば、パフォーマンスはさらに顕著になると予想されます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0