ミラーラビン素数判定
ミラーラビン素数判定は、与えられた整数が素数であるかを高速に判定する確率的アルゴリズムである。1976年にGary L. MillerがRiemannの拡張仮説の下で決定的アルゴリズムとして提案し[1]、1980年にMichael O. Rabinがこれを確率的アルゴリズムに改良した[2]。現代の暗号システムや競技プログラミングにおいて、大きな整数の素数性を効率的に判定する標準的な手法として広く採用されている。
素数判定問題は計算機科学における基本的な問題の一つであり、その歴史は古代ギリシャのエラトステネスの篩にまで遡る。しかし、RSA暗号のような現代の応用では、数百桁を超える巨大な整数に対して高速に動作するアルゴリズムが必要となる。試し割り法のような単純な手法では計算量が
理論的基礎
ミラーラビン素数判定の理論的基礎は、フェルマーの小定理とその拡張にある。フェルマーの小定理は、
しかし、フェルマーの小定理に基づく単純な素数判定には重大な問題がある。カーマイケル数と呼ばれる合成数の存在である。カーマイケル数
ミラーラビン素数判定は、この問題を回避するためにより洗練されたアプローチを採用する。
- ある
に対して
この性質は、素数
アルゴリズムの詳細
ミラーラビン素数判定のアルゴリズムは、以下の手順で構成される。まず、判定対象の整数
選択した
このテストを複数の異なる
def miller_rabin(n, k):
# Handle edge cases
if n < 2:
return False
if n == 2 or n == 3:
return True
if n % 2 == 0:
return False
# Decompose n-1 = 2^s * d
s, d = 0, n - 1
while d % 2 == 0:
s += 1
d //= 2
# Perform k rounds of testing
for _ in range(k):
a = random.randint(2, n - 2)
x = pow(a, d, n) # a^d mod n
if x == 1 or x == n - 1:
continue
for _ in range(s - 1):
x = pow(x, 2, n)
if x == n - 1:
break
else:
return False # Composite
return True # Probably prime
決定的バリアント
ミラーラビン素数判定の興味深い特性として、特定の範囲の整数に対しては、証人を適切に選ぶことで決定的なアルゴリズムとして使用できることがある。例えば、
この性質は競技プログラミングにおいて特に有用である。32ビットや64ビット整数の範囲では、少数の固定された証人を使用することで、確率的な要素を排除しつつ高速な素数判定が可能となる。以下に、実用的な決定的バリアントの実装を示す:
bool is_prime(uint64_t n) {
if (n < 2) return false;
if (n == 2 || n == 3) return true;
if (n % 2 == 0) return false;
uint64_t d = n - 1;
int s = 0;
while (d % 2 == 0) {
s++;
d /= 2;
}
// Witnesses for n < 2^64
vector<uint64_t> witnesses = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
for (uint64_t a : witnesses) {
if (a >= n) continue;
uint64_t x = mod_pow(a, d, n);
if (x == 1 || x == n - 1) continue;
bool composite = true;
for (int i = 0; i < s - 1; i++) {
x = mod_mul(x, x, n);
if (x == n - 1) {
composite = false;
break;
}
}
if (composite) return false;
}
return true;
}
実装上の注意点
ミラーラビン素数判定の実装において、最も重要な点は大きな整数の演算を正確に行うことである。特に、
64ビット整数を扱う場合、通常の乗算では128ビットの中間結果が必要となる。これを回避するために、Montgomery乗算やRussian peasant乗算などの技法が用いられる。以下は、オーバーフローを避けるためのモジュラー乗算の実装例である:
uint64_t mod_mul(uint64_t a, uint64_t b, uint64_t mod) {
uint64_t result = 0;
a %= mod;
while (b > 0) {
if (b % 2 == 1) {
result = (result + a) % mod;
}
a = (a * 2) % mod;
b /= 2;
}
return result % mod;
}
また、高速べき乗法の実装においても、各ステップでモジュラー演算を適用することが重要である。単純に
uint64_t mod_pow(uint64_t base, uint64_t exp, uint64_t mod) {
uint64_t result = 1;
base %= mod;
while (exp > 0) {
if (exp % 2 == 1) {
result = mod_mul(result, base, mod);
}
base = mod_mul(base, base, mod);
exp /= 2;
}
return result;
}
計算量とパフォーマンス
ミラーラビン素数判定の時間計算量は
各ラウンドにおいて、最も計算量の大きい操作は
実際のパフォーマンスは、使用する乗算アルゴリズムに大きく依存する。現代のプロセッサでは、64ビット整数の乗算は1命令で実行できるため、この範囲では非常に高速である。より大きな整数に対しては、Karatsuba法やFFTベースの乗算アルゴリズムを使用することで、漸近的な計算量を改善できる。
確率的側面の分析
ミラーラビン素数判定の確率的な性質を理解することは、アルゴリズムの信頼性を評価する上で重要である。合成数
具体的には、合成数
実際の合成数に対する証人の分布を調べると、多くの場合、誤認する証人の割合は
エラー確率の厳密な評価は以下のように行える。
: エラー確率 : エラー確率 : エラー確率
これらの数値は、現実的な応用において十分に小さいエラー確率を達成できることを示している。
強擬素数と証人の選択
ミラーラビン素数判定の理論において、強擬素数(strong pseudoprime)という概念が重要な役割を果たす。合成数
強擬素数の分布には興味深い性質がある。例えば、基底2に関する最小の強擬素数は2047であり、基底2と3の両方に関する最小の強擬素数は1373653である。この事実を利用して、小さな範囲の整数に対しては、少数の固定された証人で確実な判定が可能となる。
証人の選択戦略には、大きく分けて二つのアプローチがある。一つは完全にランダムな選択であり、理論的な保証が得られる。もう一つは、経験的に効果的であることが知られている特定の証人を使用する方法である。後者の例として、以下のような結果が知られている:
: 証人 で十分 : 証人 で十分 : 証人 で十分 : 証人 で十分 : 証人 で十分
これらの結果は、計算機による網羅的な探索によって得られたものである[6]。
他の素数判定法との比較
素数判定アルゴリズムの landscape において、ミラーラビン素数判定は確率的アルゴリズムの代表格として位置づけられる。他の主要な素数判定法と比較することで、その特徴がより明確になる。
試し割り法は最も単純な決定的アルゴリズムであり、
ソロベイ・シュトラッセンテストは、ミラーラビン素数判定と同様の確率的アルゴリズムである。Jacobi記号を用いた判定を行い、エラー確率は同じく
AKS素数判定は、2002年に発見された多項式時間の決定的アルゴリズムである[7]。理論的には画期的な成果だが、計算量は
楕円曲線素数証明(ECPP)は、素数性の証明書を生成する決定的アルゴリズムである。期待計算量は
応用分野における実装例
ミラーラビン素数判定は、様々な実用的な場面で活用されている。暗号システムにおけるRSA鍵生成では、大きな素数を高速に見つける必要があり、ミラーラビン素数判定が標準的に使用される。通常、ランダムに生成した大きな奇数に対してミラーラビンテストを適用し、素数が見つかるまで繰り返す。
OpenSSLやGnuPGなどの暗号ライブラリでは、ミラーラビン素数判定の高度に最適化された実装が使用されている。これらの実装では、プラットフォーム固有の最適化やアセンブリレベルの実装により、高いパフォーマンスを実現している。
競技プログラミングにおいては、素因数分解や素数カウントなどの問題でミラーラビン素数判定が活用される。特に、Pollardのρ法による素因数分解と組み合わせることで、
class PrimalityTest {
private:
using u64 = uint64_t;
using u128 = __uint128_t;
static u64 mod_mul(u64 a, u64 b, u64 mod) {
return (u128)a * b % mod;
}
static u64 mod_pow(u64 base, u64 exp, u64 mod) {
u64 result = 1;
while (exp > 0) {
if (exp & 1) result = mod_mul(result, base, mod);
base = mod_mul(base, base, mod);
exp >>= 1;
}
return result;
}
static bool miller_rabin_test(u64 n, u64 a) {
if (n <= 1) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
u64 d = n - 1;
int s = 0;
while (d % 2 == 0) {
s++;
d /= 2;
}
u64 x = mod_pow(a, d, n);
if (x == 1 || x == n - 1) return true;
for (int i = 0; i < s - 1; i++) {
x = mod_mul(x, x, n);
if (x == n - 1) return true;
}
return false;
}
public:
static bool is_prime(u64 n) {
if (n <= 1) return false;
if (n <= 3) return true;
if (n % 2 == 0) return false;
// Deterministic witnesses for n < 2^64
vector<u64> witnesses = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
for (u64 a : witnesses) {
if (a >= n) break;
if (!miller_rabin_test(n, a)) return false;
}
return true;
}
};
パフォーマンスの最適化技法
ミラーラビン素数判定の実装において、パフォーマンスを向上させるための様々な最適化技法が存在する。最も重要な最適化の一つは、Montgomery乗算の使用である。Montgomery乗算は、剰余演算を高速化する技法であり、特に同じ法での複数の演算を行う場合に効果的である。
Montgomery表現では、整数
もう一つの重要な最適化は、小さな素数による前処理である。判定対象の数が小さな素数で割り切れるかを事前にチェックすることで、多くの合成数を高速に除外できる。例えば、最初の100個程度の素数で割り切れるかをチェックするだけで、ランダムな整数の大部分を素早く判定できる。
並列化も効果的な最適化手法である。複数の証人に対するテストは独立に実行できるため、マルチコアプロセッサやGPUを活用した並列実装が可能である。特に、大量の素数判定を行う必要がある場合、バッチ処理による並列化で大幅な高速化が期待できる。
理論的な発展と未解決問題
ミラーラビン素数判定に関連する理論的な研究は現在も活発に行われている。特に興味深いのは、決定的な素数判定に必要な証人の数に関する研究である。現在知られている結果では、一般化Riemann仮説(GRH)が正しければ、
強擬素数の分布に関する研究も重要な課題である。特定の基底に関する強擬素数の密度や、複数の基底に関する強擬素数の交わりについて、より精密な評価が求められている。これらの研究は、実用的なアルゴリズムの改良にも直接的に貢献する。
計算複雑性理論の観点からは、素数判定問題がPに属することはAKSアルゴリズムによって証明されたが、より効率的な決定的アルゴリズムの存在については未解決である。ミラーラビン素数判定の平均的な計算量の解析や、特定の数の分布に対する最適な証人選択戦略なども、活発な研究分野である。
量子計算の文脈では、Shorのアルゴリズムが素因数分解を多項式時間で解けることを示しているが、素数判定に特化した量子アルゴリズムの研究も進められている。古典的なミラーラビン素数判定と量子アルゴリズムの関係性についても、理論的な興味が持たれている。
これらの理論的な発展は、単に学術的な興味にとどまらず、暗号システムの安全性評価や、より効率的な実装の開発につながる可能性を秘めている。ミラーラビン素数判定は、実用性と理論的深さを兼ね備えたアルゴリズムとして、今後も計算機科学の重要な研究対象であり続けるだろう。
Miller, Gary L. (1976). "Riemann's Hypothesis and Tests for Primality". Journal of Computer and System Sciences. 13 (3): 300-317. ↩︎
Rabin, Michael O. (1980). "Probabilistic algorithm for testing primality". Journal of Number Theory. 12 (1): 128-138. ↩︎
Alford, W. R.; Granville, Andrew; Pomerance, Carl (1994). "There are infinitely many Carmichael numbers". Annals of Mathematics. 139 (3): 703-722. ↩︎
Damgård, Ivan; Landrock, Peter; Pomerance, Carl (1993). "Average case error estimates for the strong probable prime test". Mathematics of Computation. 61 (203): 177-194. ↩︎
Jaeschke, Gerhard (1993). "On strong pseudoprimes to several bases". Mathematics of Computation. 61 (204): 915-926. ↩︎
Pomerance, Carl; Selfridge, John L.; Wagstaff, Samuel S. Jr. (1980). "The pseudoprimes to 25·10^9". Mathematics of Computation. 35 (151): 1003-1026. ↩︎
Agrawal, Manindra; Kayal, Neeraj; Saxena, Nitin (2004). "PRIMES is in P". Annals of Mathematics. 160 (2): 781-793. ↩︎
Bach, Eric (1990). "Explicit bounds for primality testing and related problems". Mathematics of Computation. 55 (191): 355-380. ↩︎