Motivation

소인수분해는 매우 어려운 과정이다. 실제로 소인수분해는 (입력 비트 수에 대해) 다항 시간에 결정론적으로 풀 수 있는 방법이 알려져 있지 않다.

그러나, 약간의 랜덤성 (확률에 대한 의존) 을 허용한다면 상당히 clever한 알고리즘을 통해 빠르게 소인수분해를 할 수 있는데, 이 방법이 바로 Pollard’s Rho(ρ) 알고리즘이다.

생일 문제 (Birthday Problem)

고등학교 확/통 교과서에도 실려있는 유명한 문제인데, 다음 문제에 답해 보자.

  • 23명의 사람이 한 방에 모여 있다. 이 중, 적어도 한 쌍 이상이 생일이 겹칠 확률은 얼마인가?

계산을 직접 해 본다면, 별로 직관적이지 못한 결론을 얻는다. 대략 50%가 넘어가는데, 365개의 생일 중 23명이 골랐을 뿐인데 이렇게 높은 확률이라는 것이 비직관적이기 때문에 이 결과를 Birthday Paradox라고도 부른다.

이 문제의 핵심은, 마구 랜덤하게 뽑으면 생각보다 많이 겹친다 라는 정보이다. 이를 이용하여, 어떻게 소인수분해를 할 수 있는지 알아보고자 한다.

구체적으로, n 정도의 사람이 필요하다는 사실을 기억하자. 즉, n개 중 k개를 뽑아서, 겹치는 쌍을 만들고자 한다면, k=O(n) 정도 뽑으면 겹치는 쌍을 기대할 수 있다. 증명은 생략.

가정

폴라드-로 알고리즘이 잘 작동하기 위해서는, 큰 수 m을 소인수분해하되, m의 가장 작은 소인수 p가 작아야 한다. 예를 들어, RSA에서 쓰는 좋은 N - 즉, 큰 소수 2개를 곱한 수에서는 그 효율이 현저히 낮다. 100자리 소수를 2개 곱해서 얻은 200자리 합성수보다는, 20자리 소수 10개를 곱해서 얻은 수를 잘 소인수분해하는 알고리즘임을 의미한다.

Algorithm

k개의 정수 u1,u2,uk 를 랜덤하게 선택하자. 이때, 생일 문제에 의해, uiujmodpi,j를 얻을 가능성이 상당히 높다. 그런데, 이는 다시 말하면 uiuj가 (일반성을 잃지 않고 양수라고 하자) mp를 공약수로 가질 가능성이 상당히 높다는 뜻이다. 우리는 두 수의 gcd를 입력 비트 수에 대한 다항 시간에 찾는 유클리드 알고리즘을 잘 알고 있으므로, m의 어떤 소인수를 빠르게 찾을 수 있다는 의미가 된다. 다시 말해, (k2)개의 uiuj들과 m의 gcd를 확인함으로서 m의 어떤 약수를 얻을 확률이 상당히 높다는 뜻이다.

그러나, O(k2) 개의 조합을 모두 검토하기는 너무 느리다. 폴라드-로 아이디어는 이 과정을 줄이는 것인데, ui를 랜덤하게 뽑는 대신 ui+1=ui2+1 을 사용하는 것이다. 이 다항식 f(x)=x2+1 은 사실 어떤 다항식이든 크게 상관은 없으나, mod p에 대한 랜덤성을 해치지 않아야 하고, 계산이 너무 오래 걸리지 않아야 한다. 일반적으로 저렇게 생긴 다항식이 잘 작동함이 알려져 있으나 x2+x+1 같은걸 쓴다고 큰 문제는 없고… 다만 1차식을 쓰면 안 된다는 사실도 잘 알려져 있다.

이런식으로 결정론적으로 계산하는 것의 장점은, ui 의 수열이 주기성을 갖게 된다. 우리가 어떤 주기성을 갖는 수열 ui에서 주기를 찾고자 할 때는 Tortoise and Hare 라는 좋은 방법을 쓸 수 있는데, 이 방법은 다음과 같다.

수열의 주기를 r이라고 할 때, stmodrus=ut 가 된다. 따라서, s를 적당히 큰 (주기성을 보이는) r의 배수로 잡고, t=2s로 잡으면 항상 usu2s 이다. 따라서, 우리는 k를 하나씩 늘리면서 u2kuk 만 확인하여도 반드시 주기를 놓치지 않음이 보장되며, 이때 주기성을 가지는 첫번째 r의 배수가 대략 O(p) 정도 스케일이기 때문에 k=O(p) 정도 확인하면 그 안에 gcd(u2kuk,m) 가 1이 아닌 k를 기대할 수 있다.

예시

Niven 의 정수론 책에 수록된 예시는 다음과 같다.

m=36,287, f(x)=x2+1modm, u0=1일 때, u의 수열을 계산하면…
1,2,5,26,677,22886,2439,33941,24380,3341,22173,25652,26685,29425,22806… 이다. 이때, 각 k에 대해 gcd(u2kuk,m) 를 계산하면, u14u7에서 gcd(u14u7,m)=131 이다. 131은 m의 약수이다.

구현

위 알고리즘을 그대로 코드로 옮기면 된다.

  • 단, u 수열을 미리 구해놓고 Tortoise-Hare를 돌리는 것은 어디까지 구해야 할지 모르는 상황에서는 별로 적절하지 않다. x2+1 은 구하기 쉬운 다항식이므로, 매 스텝마다 y는 두 스텝씩, x는 한 스텝씩 나간다고 생각하면서 진행시키자.
  • u0는 랜덤하게 뽑았는데, 별로 상관은 없다.
  • is_prime 부분은 일반적인 소수 판정 함수를 쓰면 된다. 보통은 밀러-라빈 판정법을 많이 쓴다 (그냥 판정하면 느리니까).
inline int64_t mulmod(int64_t x, int64_t y, int64_t m)
{
    return ((__int128)x * y % m);
}
int32_t PollardRho(int32_t n)
{
    if (n==1) return n;
    if (n % 2 == 0) return 2;

    int32_t x = (rand()%(n-2))+1;
    int32_t y = x;
    int32_t c = 1;
    int32_t d = 1;
    while (1)
    {
        x = (mulmod(x, x, n) + c + n)%n;

        y = (mulmod(y, y, n) + c + n)%n;
        y = (mulmod(y, y, n) + c + n)%n;

        d = __gcd(abs(x-y), n);
        if (d == 1) continue;
        if (is_prime(d))
            return d;
        else return PollardRho(d);
    }
}

시간 복잡도

앞서 설명한 바와 같이, Birthday Paradox에 의해 실제로 구해야 하는 u의 값은 O(p)개이다. n에 대해서는 pn 임을 가정하면 (소수가 아님은 밀러라빈 등으로 확인하자) O(n1/4) 알고리즘이라고 할 수 있다. 이는 다항식 x2+1 이 진짜 랜덤한 수들을 준다는 가정 하에 이루어진 계산인데, 실제로는 당연히 뭔가 더 복잡하고 끔찍한 분석이 필요하다. 이쪽은 잘 모르기도 하니 일단은 넘어가기로 하자.

당연히, 매우 큰 수를 다룰 때는 곱셈이나 모듈러 등이 유의미하게 오래 걸린다. 이 경우 적절하게 시간 복잡도에도 이런 항들을 곱해야 할 것이다. 우선은 64비트 (곱셈이 O(1)에 수행) 가정 하에서의 복잡도가 위와 같다고 알아두면 된다.