【アルゴリズム】ミラー・ラビン素性検査

14025 ワード

問題の説明
質問は、Sphere Online Judge (SPOJ) Webサイトの「Prime or Not」のテーマに由来します.
Prime or Not: Given the number, you are to answer the question: "Is it prime?"
Input:t – the number of test cases, then t test cases follows. [t ≤ 500]Each line contains one integer: N [2 ≤ N ≤ 263-1]
Output: For each test case output string "YES"if given number is prime and "NO"otherwise.
Time limit: 21s
Source limit: 5,000 Bytes
この問題は,各数が263−1を超えない約500個の与えられた数が素数であるかどうかを判断することを要求している.時間制限は21秒です.試除法で因子分解を行うと必ずタイムアウトします.ふるいほうを使用すると、メモリが限界を超えてしまうことは間違いありません.
アルゴリズムの説明
まず、素数に関するフェルマの定理を見てみましょう.
pが素数である場合、a p≡a(mod p)はすべての整数aに対して存在する.
上記のFerma定理によれば,pが素数であり,aがpの倍数でないたびに,a p−1≡1(mod p)がある.さらに,a n−1 mod nを計算する有効な方法があり,O(log n)個のモードn乗算のみが必要である.従って,この関係が成立しない場合,nは素数ではないと確信できる.与えられた数の非素性にとって,Ferma定理は強力な検証である.nが素数でない場合、a n−1≠1(mod n)となるように、a大きな数が素数かどうかを決定する有効なアルゴリズムが見つかりませんでした.しかし、Miller-Rabin primatlity testアルゴリズムは、大きな数が素数であるか否かを高い確率で検証することができる.アルゴリズムの説明は次のとおりです.
Input:
n > 3, an odd integer to be tested for primality;
Input:
k, a parameter that determines the accuracy of the test
Output:
composite if
n is composite, otherwise
probably prime
01: write
n − 1 as 2

d with
d odd by factoring powers of 2 from
n − 1
02: LOOP: repeat
k times:
03:   pick
a randomly in the range [2,
n − 2]
04:   
x ←
a
d mod
n
05:   if
x = 1 or
x =
n − 1 then do next LOOP
06:   for
r = 1 ..
s − 1
07:     
x ←
x
2 mod
n
08:     if
x = 1 then return
composite
09:     if
x =
n − 1 then do next LOOP
10:   return
composite
11: return
probably prime
このアルゴリズムを構成する考え方は,a d≠1(mod n)およびn=1+2 s・dが素数であれば値系列
a
d mod
n,
a
2d mod
n,
a
4d mod
n,…,
a
2s d mod
n
1で終わり、最初の1の前の値がn−1になる(pが素数の場合、y 2≡1(mod p)については、(y+1)(y−1)がpの倍数でなければならないため、y≡±1(mod p)という解しかない).なお、n−1がシーケンスに存在する場合、シーケンスの次の値は必ず1である.なぜなら:(n−1)2≡n 2−2 n+1≡1(mod n)であるからである.このアルゴリズムでは、
  • アルゴリズムは、3より大きい奇数nが素数であるか否かを判断するために使用される.パラメータkは、nが素数である確率を決定するために用いられる.
  • このアルゴリズムは、nが合数であることを肯定的に判断することができるが、nは素数である可能性があるとしか言いようがない.
  • 行目は、n−1を2 s・dの形式に分解し、ここでdは奇数である.
  • 行目02行目、以下の手順(03行目から10行目)をk回ループします.
  • 第03行,◇[2,n−2]の範囲で独立かつランダムに正の整数aを選択する.
  • 04行目、◇このシーケンスの最初の値:x←ad mod nを計算します.
  • 05行目、◇このシーケンスの最初の数が1またはn−1である場合、上記の条件に合致すると、nは素数である可能性があり、03行目に移動して次のサイクルを行う.
  • 06行目、◇07行目から09行目をループし、このシーケンスの残りのs-1個の値を順次ループします.
  • 07行目、◇このシーケンスの次の値:x←x 2 mod nを計算します.
  • 08行目、◇◇この値が1であれば、前の値はn-1ではなく、上記の条件に合致しないため、nは必ず合数であり、アルゴリズムは終了する.
  • 09行目、◇この値がn-1であれば、次の値は必ず1であり、上記の条件に合致し、nは素数である可能性があり、03行目に回って次のループを行う.
  • 10行目、◇このシーケンスが1で終わるのではなく、上記の条件に合致しないことが判明したので、nは合数に違いなく、アルゴリズムは終了する.
  • の11行目は、k個の独立してランダムに選択されたa値を検証したので、nが素数である可能性が高いと判断し、アルゴリズムは終了する.

  • 1回の検査では、アルゴリズムのエラーの可能性は最大4分の1である.aを独立かつランダムに選択して繰返し検査を行うと,このアルゴリズムがnが合数であることを報告すると,nは素数ではないと確信できる.しかし、このアルゴリズムが25回の検査を繰り返してnが素数である可能性があると報告した場合、nは「ほぼ素数に違いない」と言える.このような25回の検査プロセスは、その入力に関するエラー情報を与える確率が(1/4)25未満であるからである.この機会は1015分の1未満です.このような過程で10億個の異なる素数を検証しても,誤りを予想する確率は百万分の1未満である.そのため、もし本当に間違っていたら、このアルゴリズムは繰り返し推測するよりも、ハードウェアの故障や宇宙線の原因で、私たちのコンピュータはその計算の中で1人を失いました.このような確率的アルゴリズムは,従来の信頼性基準に対して,本当に素性のある厳密な証明が必要かどうかという疑問符を提出した.(以上の文字はDonald E. Knuth著『コンピュータプログラミングアート第2巻半数値アルゴリズム(第3版)』第359ページ「4.5.4分解素因子」の「アルゴリズムP(確率素性検査)」の後の説明を引用)
    Rubyプログラム
    上記のアルゴリズムに基づいて,対応するRubyプログラムを以下のように記述する.
    def modPow a, b, m
      v = 1
      p = a % m
      while b > 0 do
        v = (v * p) % m if (b & 1) != 0
        p = (p * p) % m
        b >>= 1
      end
      v
    end
    
    def witness a, n
      n1 = n - 1
      s2 = n1 & -n1
      x = modPow a, n1 / s2, n
      return false if x == 1 || x == n1
      while s2 > 1 do
        x = (x * x) % n
        return true if x == 1
        return false if x == n1
        s2 >>= 1
      end
      true
    end
    
    def probably_prime? n, k
      # http://en.wikipedia.org/wiki/Miller-Rabin_primality_test
      # n, an integer to be tested for primality
      # k, a parameter that determines the accuracy of the test
      return true if n == 2 || n == 3
      return false if n < 2 || n % 2 == 0
      k.downto(1) do
        return false if witness rand(n - 3) + 2, n
      end
      true
    end
    
    # http://www.spoj.pl/problems/PON/
    gets.to_i.downto(1) do
      puts probably_prime?(gets.to_i, 1) ? 'YES' : 'NO'
    end
    

    このサイトに提出した結果、「accepted」で、実行時間は0.23秒、メモリは4.7 MBで、現在Ruby言語では3位です.
    Cプログラム
    対応するC言語プログラムは以下の通りです.
    01:  #include <stdio.h>
    02: #include <stdlib.h>
    03: #include <time.h>
    04: 
    05: typedef unsigned long long U8;
    06:  typedef int bool;
    07:  
    08:  const bool true = 1;
    09:  const bool false = 0;
    10:  
    11:  U8 modMultiply(U8 a, U8 b, U8 m)
    12:  {
    13:    return a * b % m;
    14:  }
    15:  
    16:  U8 modPow(U8 a, U8 b, U8 m)
    17:  {
    18:    U8 v = 1;
    19:    for (U8 p = a % m; b > 0; b >>= 1, p = modMultiply(p, p, m))
    20:      if (b & 1) v = modMultiply(v, p, m);
    21:    return v;
    22:  }
    23:  
    24:  bool witness(U8 a, U8 n)
    25:  {
    26:    U8 n1 = n - 1, s2 = n1 & -n1, x = modPow(a, n1 / s2, n);
    27:    if (x == 1 || x == n1) return false;
    28:    for (; s2 > 1; s2 >>= 1)
    29:    {
    30:      x = modMultiply(x, x, n);
    31:      if (x == 1) return true;
    32:      if (x == n1) return false;
    33:    }
    34:    return true;
    35:  }
    36:  
    37:  U8 random(U8 high)
    38:  {
    39:    // http://www.cppreference.com/wiki/c/other/rand
    40:   return (U8)(high * (rand() / (double)RAND_MAX));
    41:  }
    42:  
    43:  // http://en.wikipedia.org/wiki/Miller-Rabin_primality_test
    44: // n, an integer to be tested for primality
    45: // k, a parameter that determines the accuracy of the test
    46: bool probablyPrime(U8 n, int k)
    47:  {
    48:    if (n == 2 || n == 3) return 1;
    49:    if (n < 2 || n % 2 == 0) return 0;
    50:    while (k-- > 0) if (witness(random(n - 3) + 2, n)) return false;
    51:    return true;
    52:  }
    53:  
    54:  // http://www.spoj.pl/problems/PON/
    55: int main()
    56:  {
    57:    srand(time(NULL));
    58:    int t;
    59:    scanf("%d", &t);
    60:    while (t-- > 0)
    61:    {
    62:      U8 n;
    63:      scanf("%lu", &n);
    64:      puts(probablyPrime(n, 3) ? "YES" : "NO");
    65:    }
    66:    return 0;
    67:  }

    このサイトに提出し、実行結果は「wrong answer」です.これは,ソースプログラムにおける11~14行目のmodMultiply関数演算オーバーフローによるものであり,この関数のパラメータと戻り値はいずれも問題要求の数値範囲(64 bits整数の範囲を超えない)を表すことができるが,その乗算の中間の結果は128 bitsの整数で表現され,オーバーフローをもたらす.C言語には大きな整数タイプは内蔵されていません.
    参考資料
  • Wikipedia: Fermat’s little theorem
  • Wikipedia: Miller-Rabin primality test
  • Wikipedia: Sieve of Eratosthenes
  • The Art of Computer Programming

  • アルゴリズムとデータ構造ディレクトリ