けんちょんの競プロ精進記録

競プロの精進記録や小ネタを書いていきます

ミラー・ラビンの素数判定法 (Miller-Rabin 法)

ミラー・ラビンの素数判定法は、その背景にある整数論的考察もめっちゃ面白いので、ぜひそれも味わいましょう!!

1. はじめに

本記事では正の整数  N が与えられたときに、 N が素数であるか否かを判定する問題を考えます。

よく知られた方法は、 N a = 2, 3, \dots で順に試し割りしていく方法です。 a \le \sqrt{N} まで試せばよいことから  O(\sqrt{N}) の計算量となります。競プロでは、 N \le 10^{12} 程度の制約下で、1 回だけ素数判定するのが限界というイメージがありますよね。

ところが、本記事で紹介する Miller-Rabin の素数判定法を用いると、2 秒以内の実行制限時間内に、

  •  N \le 10^{18} 程度の制約下で、
  •  Q = 10^{5} 回程度の回数の

素数判定が行えます!

例によって、Yosupo Judge Library Checker の問題「Primality Test」を通します。

なお注意点として、Miller-Rabin 法は確率的アルゴリズムであり、通常は、非常に低い確率で「合成数を素数だと誤判定してしまう」ことがあり得ます。しかし、うまくやることで、 2^{64} 以下の範囲では確率 100% で素数判定ができることが示されています。実用的には決定的アルゴリズムといってよいでしょう。

目次

 

2. 素数の性質

Miller-Rabin 法を理解する上で欠かせない、素数の重要な性質を 1 つ挙げます。


【素数の性質 1】

 a, b, c, \dots を整数とし、 p を素数とする。

 abc \dots p の倍数であるならば、 a, b, c, \dots のうちの少なくとも 1 つは  p の倍数である。


一見すると当然のようにも思える定理ですが、意外と自明ではなく、奥が深い定理です。有名な「素因数分解の一意性」にも深く関わってくる定理です。ここでは証明はしないですが、関心のある方は高木貞治先生の『初等整数論講義』などを読んでみてください。

もう一つ、Miller-Rabin 法を理解する上で欠かせない素数の定理として、Fermat の小定理があります。


【素数の性質 2:Fermat の小定理】

 p が素数とし、 a p で割り切れない整数とする。このとき、

 a^{p-1} \equiv 1 \pmod p

が成立する。


Fermat の小定理については、次の記事に詳しく書きました!

qiita.com

Fermat の小定理も、その証明方法も含めて理解することで、整数と仲良くなれる。そんな定理です。

 

3. Miller-Rabin 法の原理

Miller-Rabin 法の原理となる整数論的性質を深掘りしていきます。ここまでの素数の性質が頭にしっかり染み込んでいれば、Miller-Rabin 法の原理はいたって単純です!

たとえば、 p = 41 としましょう。このとき、Fermat の小定理より、 a p で割り切れない整数として

 a^{40} \equiv 1 \pmod{41}

が成り立ちます。1 を右辺に移行して因数分解してみましょう。

 (a^{20} + 1)(a^{10} + 1)(a^{5} + 1)(a^{5} - 1) \equiv 0 \pmod{41}

となります。これは  (a^{20} + 1)(a^{10} + 1)(a^{5} + 1)(a^{5} - 1) が 41 の倍数であることを意味しているので、素数の性質 1 より、これら 4 つの値のうちの少なくとも 1 つが 41 の倍数であることになります。つまり、

  •  a^{5} \equiv 1 \pmod{41}
  •  a^{5} \equiv -1 \pmod{41}
  •  a^{10} \equiv -1 \pmod{41}
  •  a^{20} \equiv -1 \pmod{41}

のうちのいずれかが成り立つことになります。もしすべて成り立たないとするならば、41 は素数ではないと言い切れることになります。

一般の整数  N の場合

続いて、素数かどうかを判定したい一般の整数  N に対して、同様のことを考えてみましょう。

 N が素数であるならば、Fermat の小定理より、 N の倍数でない整数  a に対して

 a^{N-1} \equiv 1 \pmod{N}

が成り立ちます。先ほどと同様に、1 を右辺に移して因数分解します。 N-1 = 2^{s}d ( d は奇数) と表すことにすると、

 (a^{2^{s-1}d} + 1)(a^{2^{s-2}d} + 1)(a^{2^{s-3}d} + 1) \dots (a^{d} + 1)(a^{d} - 1) \equiv 0 \pmod{N}

となります。したがって、 N が素数であるならば、

  •  a^{d} \equiv 1 \pmod{N}
  •  a^{d} \equiv -1 \pmod{N}
  •  a^{2d} \equiv -1 \pmod{N}
  •  \dots
  •  a^{2^{s-2}d} \equiv -1 \pmod{N}
  •  a^{2^{s-1}d} \equiv -1 \pmod{N}

のうちのいずれかが成り立つことになります。

逆に、これらがどれも成り立たないとき、 N は合成数であると言いきれます。ただし、上記の式のうちのいずれかが成り立つ場合に、 N が素数だと言い切れるわけではないことには注意しましょう。

ここで、以下の定理が知られています。証明は 37zigen さんの記事にあります。つまり、 a をランダムに選ぶのを  k 回実施してテストすれば、合成数を誤って素数と判定してしまう確率を  (\frac{1}{4})^{k} 以下にできると言えます。


 N が奇数の合成数とし、 a 1 以上  N-1 以下の整数の中からランダムに選ぶとする。

このとき、75% 以上の確率で

  •  a^{d} \not \equiv 1 \pmod{N}
  •  a^{d} \not\equiv -1 \pmod{N}
  •  a^{2d} \not\equiv -1 \pmod{N}
  •  \dots
  •  a^{2^{s-2}d} \not\equiv -1 \pmod{N}
  •  a^{2^{s-1}d} \not\equiv -1 \pmod{N}

が成立する。


数値例

 N = 97 (素数)

 N = 97 ( N - 1 = 2^{5} \times 3) のとき、 a = 2 とすると、

 (2^{3}, 2^{6}, 2^{12}, 2^{24}, 2^{48}) \equiv (64, 22, 96, 1, 1) \pmod{97}

となります。 2^{12} \equiv -1 \pmod{97} となっています。

 N = 161 (合成数)

 N = 161 ( N - 1 = 2^{5} \times 5) のとき、 a = 2 とすると、

 (2^{5}, 2^{10}, 2^{20}, 2^{40}, 2^{80}) \equiv (58, 144, 128, 123, 156) \pmod{97}

となります。この時点で  N = 161 は合成数だと断定できます。

 N = 2047 (合成数)

 N = 2047 ( N - 1 = 2 \times 1023) のとき、 a = 2 とすると、

 2^{1023} \equiv 1 \pmod{2047}

となります。これだけでは  N = 2047 は素数とも合成数とも言い切れません。しかし  a = 7 とすると、

 7^{1023} \equiv 942 \pmod{2047}

となります。これによって、 N = 2047 は合成数であると言い切れます。

 

4. Miller-Rabin 法

Miller-Rabin の素数判定法をまとめます。


  •  N = 2 のとき:素数である
  •  N 2 以外の偶数であるとき:合成数である

以下、 N 3 以上の奇数とする。 N -1 = 2^{s} d ( d は奇数) と表す。

 1 以上  N-1 以下の整数  a に対して、

  •  a^{d} \equiv 1 \pmod{N}
  •  a^{d} \equiv -1 \pmod{N}
  •  a^{2d} \equiv -1 \pmod{N}
  •  \dots
  •  a^{2^{s-2}d} \equiv -1 \pmod{N}
  •  a^{2^{s-1}d} \equiv -1 \pmod{N}

のいずれかが成り立つことを「 a N のテストに通る」と呼ぶことにする。 N のテストに通らない  a が検出されたならば、直ちに  N は合成数であると言える。

ランダムに  a を選択することを  k 回繰り返して、いずれの  a もテストに通ることが確かめられた場合、確率  1 - (\frac{1}{4})^{k} N は素数であると言える。


Miller-Rabin 法は、素数を誤って合成数と判定してしまうことは絶対にないですが、合成数を誤って素数と判定してしまうことはあり得ます。しかし、試行回数  k を増やせば、誤判定確率を減らせます。

計算量は、

  •  \mod{N} での整数の乗算 1 回:計算量  O((\log N)^{2})
  • 乗算の回数: O(k \log N)

ということで、全体で  O(k (\log N)^{3}) と評価できます。しかし、競プロ的には、 N \le 10^{18} の範囲における  \mod{N} での整数の乗算は定数時間でできると考える方が自然で、実質的な計算量は  O(k \log N) となります。

 

注意: N が小さいときは決定的アルゴリズムに

このように Miller-Rabin 法は確率的アルゴリズムですが、十分小さい  N に対しては、テストに用いる  a を上手に選ぶことで決定的アルゴリズムとなります。


  •  N \lt 4759123141 のとき: a = 2, 7, 61
  •  N \le 2^{64} のとき: a = 2, 325, 9375, 28178, 450775, 9780504, 1795265022

に対してテストに通るならば、 N は素数であると言い切れる


なお、効率的な  a の選び方について、次のサイトによくまとまっています。

miller-rabin.appspot.com

 

5. Miller-Rabin 法の実装

Miller-Rabin 法はとてもシンプルなので、速度を気にしなければ実装は容易です。

しかし、「 a^{d} N で割った余り」を求める部分をどのように実装するかによって、結構な速度差が生じます。Yosupo Judge の Primality Test をベンチマークとしてみましょう。

 a^{d} N で割った余り」を計算するときには、どうしても 64 bit 整数の範囲におさまらない値が出てきます。その対策として、次の 2 つの方法を実装してみました。

  • 方法 1:単純に __uint128_t 型を用いる方法 (僕の実装で 1783ms)
  • 方法 2:モンゴメリ乗算を用いる方法 (僕の実装で 247ms)

 

方法 1:__uint128_t 型を用いる方法

 ab \pmod{N} を計算するとき、 ab の値は 64 bit 整数におさまりません。そこで、__uint128_t 型を用います。次のコードは、Yosupo Judge で 1783ms でした。

#include <bits/stdc++.h>
using namespace std;

// A^N mod M
template<class T> T pow_mod(T A, T N, T M) {
    T res = 1 % M;
    A %= M;
    while (N) {
        if (N & 1) res = (res * A) % M;
        A = (A * A) % M;
        N >>= 1;
    }
    return res;
}

bool MillerRabin(long long N, vector<long long> A) {
    long long s = 0, d = N - 1;
    while (d % 2 == 0) {
        ++s;
        d >>= 1;
    }
    for (auto a : A) {
        if (N <= a) return true;
        long long t, x = pow_mod<__int128_t>(a, d, N);
        if (x != 1) {
            for (t = 0; t < s; ++t) {
                if (x == N - 1) break;
                x = __int128_t(x) * x % N;
            }
            if (t == s) return false;
        }
    }
    return true;
}

bool is_prime(long long N) {
    if (N <= 1) return false;
    if (N == 2) return true;
    if (N % 2 == 0) return false;
    if (N < 4759123141LL)
        return MillerRabin(N, {2, 7, 61});
    else
        return MillerRabin(N, {2, 325, 9375, 28178, 450775, 9780504, 1795265022});
}

void YosupoPrimarityTest() {
    int N;
    cin >> N;
    for (int i = 0; i < N; ++i) {
        long long A;
        cin >> A;
        if (is_prime(A))
            cout << "Yes" << endl;
        else
            cout << "No" << endl;
    }
}

int main() {
    YosupoPrimarityTest();
}

 

方法 2:モンゴメリ乗算を用いる方法

モンゴメリ乗算は、時間のかかる除算を実質的に行うことなく、乗算・加算・減算・シフト演算のみで、効率的に整数の積の剰余を求めることのできるアルゴリズムです。

ここでは詳細は省略しますが、モンゴメリ乗算を用いると、32 bit 整数に収まらない整数を法とする効率的な modint を作れます。

次のコードは、Yosupo Judge で 247ms でした。特別なチューニングを施していない単純な実装としては、まずまずの速度だと思います。

#include <bits/stdc++.h>
using namespace std;

// montgomery modint (MOD < 2^62, MOD is odd)
struct MontgomeryModInt64 {
    using mint = MontgomeryModInt64;
    using u64 = uint64_t;
    using u128 = __uint128_t;
    
    // static menber
    static u64 MOD;
    static u64 INV_MOD;  // INV_MOD * MOD ≡ 1 (mod 2^64)
    static u64 T128;  // 2^128 (mod MOD)
    
    // inner value
    u64 val;
    
    // constructor
    MontgomeryModInt64() : val(0) { }
    MontgomeryModInt64(long long v) : val(reduce((u128(v) + MOD) * T128)) { }
    u64 get() const {
        u64 res = reduce(val);
        return res >= MOD ? res - MOD : res;
    }
    
    // mod getter and setter
    static u64 get_mod() { return MOD; }
    static void set_mod(u64 mod) {
        assert(mod < (1LL << 62));
        assert((mod & 1));
        MOD = mod;
        T128 = -u128(mod) % mod;
        INV_MOD = get_inv_mod();
    }
    static u64 get_inv_mod() {
        u64 res = MOD;
        for (int i = 0; i < 5; ++i) res *= 2 - MOD * res;
        return res;
    }
    static u64 reduce(const u128 &v) {
        return (v + u128(u64(v) * u64(-INV_MOD)) * MOD) >> 64;
    }
    
    // arithmetic operators
    mint operator - () const { return mint() - mint(*this); }
    mint operator + (const mint &r) const { return mint(*this) += r; }
    mint operator - (const mint &r) const { return mint(*this) -= r; }
    mint operator * (const mint &r) const { return mint(*this) *= r; }
    mint operator / (const mint &r) const { return mint(*this) /= r; }
    mint& operator += (const mint &r) {
        if ((val += r.val) >= 2 * MOD) val -= 2 * MOD;
        return *this;
    }
    mint& operator -= (const mint &r) {
        if ((val += 2 * MOD - r.val) >= 2 * MOD) val -= 2 * MOD;
        return *this;
    }
    mint& operator *= (const mint &r) {
        val = reduce(u128(val) * r.val);
        return *this;
    }
    mint& operator /= (const mint &r) {
        *this *= r.inv();
        return *this;
    }
    mint inv() const { return pow(MOD - 2); }
    mint pow(u128 n) const {
        mint res(1), mul(*this);
        while (n > 0) {
            if (n & 1) res *= mul;
            mul *= mul;
            n >>= 1;
        }
        return res;
    }

    // other operators
    bool operator == (const mint &r) const {
        return (val >= MOD ? val - MOD : val) == (r.val >= MOD ? r.val - MOD : r.val);
    }
    bool operator != (const mint &r) const {
        return (val >= MOD ? val - MOD : val) != (r.val >= MOD ? r.val - MOD : r.val);
    }
    friend istream& operator >> (istream &is, mint &x) {
        long long t;
        is >> t;
        x = mint(t);
        return is;
    }
    friend ostream& operator << (ostream &os, const mint &x) {
        return os << x.get();
    }
    friend mint modpow(const mint &r, long long n) {
        return r.pow(n);
    }
    friend mint modinv(const mint &r) {
        return r.inv();
    }
};

typename MontgomeryModInt64::u64
MontgomeryModInt64::MOD, MontgomeryModInt64::INV_MOD, MontgomeryModInt64::T128;

// Miller-Rabin
bool MillerRabin(long long N, vector<long long> A) {
    using mint = MontgomeryModInt64;
    mint::set_mod(N);
    
    long long s = 0, d = N - 1;
    while (d % 2 == 0) {
        ++s;
        d >>= 1;
    }
    for (auto a : A) {
        if (N <= a) return true;
        mint x = mint(a).pow(d);
        if (x != 1) {
            long long t;
            for (t = 0; t < s; ++t) {
                if (x == N - 1) break;
                x *= x;
            }
            if (t == s) return false;
        }
    }
    return true;
}

bool is_prime(long long N) {
    if (N <= 1) return false;
    else if (N == 2) return true;
    else if (N % 2 == 0) return false;
    else if (N < 4759123141LL)
        return MillerRabin(N, {2, 7, 61});
    else
        return MillerRabin(N, {2, 325, 9375, 28178, 450775, 9780504, 1795265022});
}

void YosupoPrimarityTest() {
    cin.tie(0);
    ios::sync_with_stdio(false);
    int N;
    cin >> N;
    for (int i = 0; i < N; ++i) {
        long long A;
        cin >> A;
        if (is_prime(A))
            cout << "Yes" << endl;
        else
            cout << "No" << endl;
    }
}

int main() {
    YosupoPrimarityTest();
}

 

6. おわりに

ここまで、Miller-Rabin 法の原理と実装を見てきました。Miller-Rabin 法の原理は、それ自体が整数論的にとても面白いと思います。みんなもぜひ素敵な整数論ライフを!