[Algo] 폴라드 로(Pollard Rho) 인수분해 알고리즘

Park Yeongseo·2024년 4월 5일
0

Algorithm

목록 보기
7/19
post-thumbnail

폴라드 로 알고리즘은 1974년 폴라드가 만들어 낸, 합성수의 인수를 찾는 알고리즘이다. 이전에 작성한 이 글을 읽고 오기를 추천한다.

David.M.Burton의 Elementary Number Theory의 내용을 정리한 내용입니다.

1. 폴라드 로 알고리즘 (기본)

합성수로 알려진 큰 홀수 nn이 있다고 하자. 폴라드 로 알고리즘은 정수 계수를 가진, 최소 2차 이상의 간단한 다항식을 정하는 것으로부터 시작한다.

f(x)=x2+a,a0,2f(x) = x^2 + a, a \neq 0, -2

함수 f(x)f(x)는 임의의 값 x0x_0 으로부터 시작해, 다음의 관계를 만족하는 유사 난수 수열 x1, x2, x3, ...x_1,\ x_2,\ x_3,\ ... 을 재귀적으로 만들어 내는 데 쓰인다.

xk+1f(xk)(modn)k=0, 1, 2, ...x_{k+1} \equiv f(x_k) (\mod n) \qquad k = 0,\ 1,\ 2,\ ...

정수 dd가 1이 아닌, nn보다 작은 nn의 약수라 하자. d<nd < n이므로 dd를 법으로 하는 합동류(congruence class)는 nn을 법으로 하는 합동류보다 작다. 그렇기 때문에 위 수열에는 다음을 만족하는 법 dd에 대해서는 같은 합동류에 속하지만 법 nn에 대해서는 다른 합동류에 속하는 xk, xjx_k,\ x_j 쌍이 있을 수 있다. 즉 다음을 만족하는 xk, xjx_k,\ x_j가 있을 수 있다.

xkxj(modd)xk≢xj(modn)\begin{aligned} x_k \equiv x_j(\mod d)\\ x_k \not\equiv x_j(\mod n) \end{aligned}

xkx_kxjx_j가 나머지 dd에 대해 합동이라는 것은 ddxkxjx_k - x_j를 나눈다는 것이다. 한편 xkx_kxjx_j는 법 nn에 대해서는 합동이 아니므로, nnxkxjx_k - x_j를 나누지 않는다. 따라서 gcd(xkxj,n)gcd(x_k - x_j, n)은 1이 아닌 nn의 약수이다.

dn이므로 n=da이고, d(xkxj)이므로 (xkxj)=db라 할 수 있다.(a, b는 정수)n∤(xkxj)이므로 gcd(xkxj,n)은 d보다 크거나 같은, n이 아닌 정수다.\begin{aligned} &d | n이므로\ n= da이고,\ d | (x_k - x_j)이므로\ (x_k - x_j) = db라\ 할\ 수\ 있다.(a,\ b는\ 정수)\\ &n \not\mid (x_k-x_j)이므로\ gcd(x_k-x_j, n)은\ d보다\ 크거나\ 같은,\ n이\ 아닌\ 정수다. \end{aligned}

다만 실제로는 nn의 약수 dd에 대해서는 이외에 미리 알려진 게 없다. 하지만 정수 xkx_k를 계속 따라가다 보면 dd를 찾을 수 있을지도 모른다. xkx_kxjx_j (단 k>jk > j)를 비교하며 1이 아닌 gcd(xkxj,n)gcd(x_k - x_j, n)이 나올 때까지 반복해보자.

그러나 이렇게 얻은 최대 공약수가 반드시 nn가장 작은 인수라는 보장은 없고, 더욱이 소수라는 보장은 없다. 또한 계산을 이어갔을 때 드물게 nn의 진약수가 아닌 nn이 나올 가능성도 있다. 이때는 다른 x0x_0 값이나 다른 다항식 f(x)f(x)를 사용해서 해결할 수 있다.

Example(1)

정수 n=2189n = 2189, x0=1x_0 = 1, f(x)=x2+1f(x) = x^2 + 1이라 하자. 이때 나타나는 수열은 다음과 같다.

x1=2, x2=5, x3=26, x4=677, x5=829, ...x_1 = 2,\ x_2 = 5,\ x_3 = 26,\ x_4 = 677,\ x_5 = 829,\ ...

수열에 나타나는 서로 다른 xkx_k를 서로 비교해보면 gcd(x5x3,2189)=gcd(803,2189)=11gcd(x_5 - x_3, 2189) = gcd(803, 2189) = 11을 찾을 수 있다. 2189÷11=1992189 \div 11 = 199이므로 111121892189의 인수임을 확인할 수 있다.

(CODE) 기본 폴라드 로

아래는 위에서 설명한 기본 폴라드 로를 구현한 코드다. 다만 int의 오버플로와 같은 문제는 따로 처리하고 있지는 않다.

#include <iostream>
#include <vector>
using namespace std;

vector<int> x;

int f(int x_k, int a, int n){//간단한 다항식
    return (x_k * x_k + a) % n;
}

int gcd(int a, int b){
    a = abs(a);
    b = abs(b);

    int r;

    if (a < b) swap(a, b);

    while (b){
        r = a % b;
        a = b;
        b = r;
    }

    return a;
}

int pollard_rho(int n){
    if (n == 1) return n;

    int a = rand() % (n - 2) + 2; // a는 n보다 작은, -2, 0이 아닌 정수여야 함 
    int x_0 = rand() % 20; //임의의 초항을 정한다

    x.push_back(x_0);//초항을 넣어주자

    while (true){
        int x_k = f(x.back(), a, n);//다음 항을 계산

        for (int x_j : x){//이전에 나온 모든 항들에 대해
            int g = gcd(x_k - x_j, n);//최대공약수를 계산해보자.

			if (g == n) {//드물게 일어나는 경우. 다시 계산해봐야 한다.
                x.clear();
                return pollard_rho(n);//다른 초항과 상수항으로 다시 시작해보자
            }
			else if (g != 1) {//non-trivial factor를 찾은 경우
                return g;
            }
        }

        x.push_back(x_k);
    }

    return n;
}

int main(){
    int n;//n은 합성수인 홀수.
    scanf("%d", &n);

    int p = pollard_rho(n);

    printf("%d %% %d = %d", n, p, n % p);
}

2. 최적화

문제는 kk가 커지면, 모든 j<kj < k에 대해 gcd(xkxj,n)gcd(x_k - x_j, n)를 찾는 일이 너무 부담스러워진다는 것이다(위 코드의 39번 줄). 이를 보다 최적화하기 위해, k=2jk = 2j인 경우만을 고려해도 됨을 보이도록 하자.

ddnn보다 작은, 1이 아닌 nn의 약수라 하자. 만약 xkxj(modd)x_k \equiv x_j (\mod d)이면 f(x)f(x)가 선택된 방식에 따라,

xj+1=f(xj)f(xk)=xk+1(modd)x_{j+1} = f(x_j) \equiv f(x_k) = x_{k+1} (\mod d)

이다. 다시 말해 수열 {xk}\{x_k\}modulo dmodulo\ d를 적용한 수열은 언젠가 길이 kjk-j 의 구간이 무한히 반복되는 수열이 된다. 즉 만약 rs(mod(kj))r \equiv s (\mod{(k -j)}) (rj,sj)(r \geq j, s \geq j) 이면, xrxs(modd)x_r \equiv x_s(\mod{d})이다. 특히 ttjj보다 큰 kjk-j의 배수이면, x2txt(modt)x_{2t} \equiv x_t(\mod{t})가 되고, 따라서 1<gcd(x2kxk,n)<n1 < gcd(x_{2k} - x_k, n) < n인 정수 k가 있을 것이라 기대할 수 있다.

유일한 단점은 n의 자명하지 않은 인수인 gcd(xixj,n)gcd(x_i - x_j, n)가 처음으로 나타나는 때가 언제인지 바로 알 수 있는 것은 아니라는 점이다.

Example (2)

n=30623n = 30623을 인수분해하려 한다고 하자. x0=3,f(x)=x21x_0 = 3, f(x) = x^2 - 1이라 하면, 이로부터 만들 수 있는 수열은 다음과 같다.
8,63,3968,4801,21104,2856,18319,18926,...8, 63, 3968, 4801, 21104, 2856, 18319, 18926, ...
x2kx_{2k}xkx_k를 비교해보면 다음을 얻을 수 있다.

x2x1=55gcd(55,n)=1x4x2=4738gcd(55,n)=1x7x3=24558gcd(24558,n)=1x8x4=14125gcd(14125,n)=113\begin{aligned} &x2 -x1 = 55 \quad gcd(55, n) = 1\\ &x4 - x2 = 4738 \quad gcd(55, n) = 1\\ &x7-x3 = 24558 \quad gcd(24558, n) = 1\\ &x8 - x4 = 14125 \quad gcd(14125, n) = 113\\ \end{aligned}

113×271=30623113 \times 271 = 30623이므로 113이 30623의 인수임을 알 수 있다.

한편 위 수열에 각각 mod113\mod{113}을 해주면 다음의 새로운 수열을 얻을 수 있다.
8,63,13,55,86,50,13,55,...8, 63, 13, 55, 86, 50, 13, 55, ...
이 수열에서는 13,55,86,5013, 55, 86, 50이 계속해서 반복된다. 이를 그림으로 나타내면 다음과 같다.

이 모양은 그리스 문자 ρ\rho와 닮아 있다. 여기서 소개한 폴라드의 인수분해 알고리즘을 가리켜 "폴라드 로 알고리즘"이라 부르는 이유다.

(CODE) 최적화 폴라드 로 코드

int pollard_rho(int n){
    if (n == 1) return n;

    int a = rand() % (n - 2) + 2; // a는 n보다 작은, -2, 0이 아닌 정수다. 
    int x_0 = rand() % 20; //임의의 초항을 정한다

    x.push_back(x_0);//초항을 넣어주자

    while (true){
        x.push_back(f(x.back(), a, n));//다음 항을 계산하고 넣어주기

        int k = x.size() - 1;
        if (k % 2) continue;//홀수 번째 항이라면 건너뛴다.

        int g = gcd(x[k] - x[k / 2], n);
        
        if (g == n) {//드물게 일어나는 경우. 다시 계산해봐야 한다.
            x.clear();
            return pollard_rho(n);//다른 초항과 상수항으로 다시 시작해보자
        }
        else if (g != 1) {//non-trivial factor를 찾은 경우
            printf("g: %d\n", g);
            return g;
        }
    }

    return n;
}

5. 남은 문제

폴라드 로 알고리즘에서 인수분해할 nn은 "합성수인 홀수"여야 한다. 위 코드를 실행해보면 알겠지만 nn이 소수인 경우는 제대로 동작하지 않는다.

따라서 임의로 입력되는 nn이 합성수인지 아닌지도 모르는 경우에는 이를 판별할 방법도 있어야 한다. 임의의 정수 nn의 소수 여부를 확인하기 위한 알고리즘으로는 다음으로 포스팅될 밀러-라빈 소수판별법(Miller-Rabin primality test)이 있고, 이 알고리즘을 폴라드 로와 조합해 임의의 큰 정수 nn의 소인수분해도 할 수 있다.

0개의 댓글