devonnuri.wiki

밀러-라빈 소수 판별법

입력 2025-02-16 08:22:56

수정 2025-04-18 14:10:18

한 양의 정수 이 소수임을 판별하는 방법을 떠올려보자. 대충 2부터 까지 돌려보면서 약수가 있는지 확인해보면 될 것 같다. 이라니, 너무 느리다. 좀 더 빠른 방법이 없을까?

밀러-라빈 소수 판정법은 비결정적으로 소수를 판정하는 알고리즘이다. 그러나 long long 범위 내에서는 결정적으로 판정할 수 있어, 비로소 PS에 사용할 수 있게 된다. 우선 페르마 소수 판별법에 대해 알아보자.

TOC

  1. 페르마 소수 판별법
  2. 밀러-라빈 소수 판별법
    1. 결정론적인 버전
  3. 참고 문헌

페르마 소수 판별법

페르마의 소정리를 이용하여 소수를 판별하는 것이다. 페르마의 소정리에 대해 알아보자.

정리페르마의 소정리

가 소수이고, 와 서로소인 정수라면, 이다.

페르마의 소정리에 대한 증명은 여럿 있지만, 그 중 완전한 잉여류 집합을 활용한 증명을 소개하겠다.

증명

가 소수이고 와 서로소인 정수라고 하자. 다음과 같은 곱셈군을 생각해보자.

각 원소를 모듈로 로 보면, 의 각 원소는 의 어떤 순열이다. 즉, 서로 다른 값이며, 로 나눈 나머지가 1부터 까지의 모든 수를 포함한다.

이제 의 모든 원소를 곱하면 다음과 같다.

왼쪽에서 을 인수로 묶을 수 있으므로,

이다. 양변에서 와 서로소이므로 약분할 수 있다.

따라서, 페르마의 소정리가 성립한다.

범위의 정수 를 선택해서 의 소수성 판별에 사용할 수 있을 것이다. 하지만, 치명적인 문제가 있다. 이면 가 소수가 아님을 알 수 있지만, 페르마의 소정리를 만족한다고 해서 소수라고 단정할 수 없다. 따라서, 어떤 정수 에 대해 를 만족하는 정수 유사소수pseudoprime라고 한다. 그렇다면 가능한 모든 에 대해 만족하는지 확인해보면 되지 않을까? 아쉽게도 모든 정수 에 대해 페르마의 소정리를 만족하지만 소수가 아닌 가 있다. 이를 카마이클 수Carmichael number라고 한다.

밀러-라빈 소수 판별법

밀러-라빈 소수 판별법은 페르마 판별법을 확장한 것이다.

홀수 에 대해 은 짝수이므로, 2의 거듭제곱으로 묶을 수 있다. 따라서, 다음과 같이 적을 수 있다.

이는 페르마의 소정리의 합동식을 인수분해할 수 있도록 한다.

이 소수라면 위 인수 중 하나가 으로 나누어떨어져야 할 것이다. 밀러-라빈 판별법은 바로 이 점을 검증하는 것이며, 따라서 페르마 판별법보다 더 엄밀하다. 인 밑 에 대해

이나 에 대해

가 성립하는 지를 확인한다.

어떤 밑 가 위 합동식 중 하나라도 만족하지 않는다면 이 합성수라는 증거를 찾은 것이다.

페르마 판별법와 유사하게, 합성수에 대해 위의 식들이 모두 만족할 가능성도 있다. 이 경우 해당 밑 강한 거짓 증거strong liar라고 부른다. 즉, 밑 가 위 식 중 하나를 만족한다고 해서 이 반드시 소수임을 밝힌 것은 아니며, 단지 이 높은 확률로 소수임을 의미한다. 하지만 카마이클 수와 같이 모든 비자명한 밑수가 거짓 증거인 경우는 존재하지 않으며, 실제로 최대 의 밑만이 강한 거짓 증거가 될 수 있음을 보일 수 있다. 만약 이 합성수라면, 무작위로 선택한 밑수가 합성수를 증명할 확률은 최소 75%이다. 서로 다른 무작위 밑수를 선택하여 여러 번 반복하면 해당 수가 소수인지 합성수인지를 매우 높은 확률로 판별할 수 있다.

다음은 64비트 정수에 대해 밀러-라빈 소수 판별법을 구현한 것이다.

using u64 = uint64_t;
using u128 = __uint128_t;

u64 binpower(u64 base, u64 e, u64 mod) {
  u64 result = 1;
  base %= mod;
  while (e) {
    if (e & 1)
      result = (u128)result * base % mod;
    base = (u128)base * base % mod;
    e >>= 1;
  }
  return result;
}

bool check_composite(u64 n, u64 a, u64 d, int s) {
  u64 x = binpower(a, d, n);
  if (x == 1 || x == n - 1)
    return false;
  for (int r = 1; r < s; r++) {
    x = (u128)x * x % n;
    if (x == n - 1)
      return false;
  }
  return true;
};

bool MillerRabin(u64 n, int iter=5) { // n이 아마도 소수이면 true, 아니면 false를 반환.
  if (n < 4)
    return n == 2 || n == 3;

  int s = 0;
  u64 d = n - 1;
  while ((d & 1) == 0) {
    d >>= 1;
    s++;
  }

  for (int i = 0; i < iter; i++) {
    int a = 2 + rand() % (n - 3);
    if (check_composite(n, a, d, s))
      return false;
  }
  return true;
}

밀러-라빈 테스트를 수행하기 전에, 먼저 몇 개의 소수들이 약수인지 추가로 검사할 수 있다. 대부분의 합성수는 매우 작은 소인수를 가지므로, 이를 통해 테스트 속도를 크게 향상시킬 수 있다. 예를 들어, 전체 수의 보다 작은 소인수를 갖는다.

결정론적인 버전

밀러는 보다 작거나 같은 모든 밑만 검사함으로써 알고리즘을 결정론적으로 만들 수 있음을 보였다. 이후 Bach가 구체적인 경계를 제시했는데, 모든 밑 만 검사하면 충분하다는 것이었다. 그러나 이는 여전히 많은 밑을 검사해야 함을 의미하므로, 더 낮은 경계를 찾기 위해 많은 계산 자원이 투자되었다.

결과적으로 32비트 정수를 판별할 때는 첫 4개의 소수 밑, 즉 2, 3, 5, 7만 검사하면 충분하다. 이 테스트를 통과하지 못하는 가장 작은 합성수는

이다.

또한 64비트 정수를 판별할 때는 첫 12개의 소수 밑수, 즉 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37만 검사하면 충분하다.

다음은 결정론적 밀러-라빈 소수 판별법의 구현이다.

bool MillerRabin(u64 n) { // n이 소수이면 true, 아니면 false를 반환.
  if (n < 2)
    return false;

  int r = 0;
  u64 d = n - 1;
  while ((d & 1) == 0) {
    d >>= 1;
    r++;
  }

  for (int a : {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37}) {
    if (n == a)
      return true;
    if (check_composite(n, a, d, r))
      return false;
  }
  return true;
}

또한 7개의 밑수, 즉 2, 325, 9375, 28178, 450775, 9780504, 1795265022만으로 검사를 수행할 수도 있다. 하지만 이 숫자들(2를 제외하고)은 소수가 아니므로, 검사하는 수가 이들 밑수의 소인수인 2, 3, 5, 13, 19, 73, 193, 407521, 299210837 중 하나와 같은지도 추가로 확인해야 한다.

참고 문헌

  1. jakobkogler, et al. Primality tests. cp-algorithms.com.