폴라드 로 알고리즘은 1974년 폴라드가 만들어 낸, 합성수의 인수를 찾는 알고리즘이다. 이전에 작성한 이 글을 읽고 오기를 추천한다.
David.M.Burton의 Elementary Number Theory의 내용을 정리한 내용입니다.
합성수로 알려진 큰 홀수 이 있다고 하자. 폴라드 로 알고리즘은 정수 계수를 가진, 최소 2차 이상의 간단한 다항식을 정하는 것으로부터 시작한다.
함수 는 임의의 값 으로부터 시작해, 다음의 관계를 만족하는 유사 난수 수열 을 재귀적으로 만들어 내는 데 쓰인다.
정수 가 1이 아닌, 보다 작은 의 약수라 하자. 이므로 를 법으로 하는 합동류(congruence class)는 을 법으로 하는 합동류보다 작다. 그렇기 때문에 위 수열에는 다음을 만족하는 법 에 대해서는 같은 합동류에 속하지만 법 에 대해서는 다른 합동류에 속하는 쌍이 있을 수 있다. 즉 다음을 만족하는 가 있을 수 있다.
와 가 나머지 에 대해 합동이라는 것은 가 를 나눈다는 것이다. 한편 와 는 법 에 대해서는 합동이 아니므로, 은 를 나누지 않는다. 따라서 은 1이 아닌 의 약수이다.
다만 실제로는 의 약수 에 대해서는 이외에 미리 알려진 게 없다. 하지만 정수 를 계속 따라가다 보면 를 찾을 수 있을지도 모른다. 와 (단 )를 비교하며 1이 아닌 이 나올 때까지 반복해보자.
그러나 이렇게 얻은 최대 공약수가 반드시 의 가장 작은 인수라는 보장은 없고, 더욱이 소수라는 보장은 없다. 또한 계산을 이어갔을 때 드물게 의 진약수가 아닌 이 나올 가능성도 있다. 이때는 다른 값이나 다른 다항식 를 사용해서 해결할 수 있다.
정수 , , 이라 하자. 이때 나타나는 수열은 다음과 같다.
수열에 나타나는 서로 다른 를 서로 비교해보면 을 찾을 수 있다. 이므로 은 의 인수임을 확인할 수 있다.
아래는 위에서 설명한 기본 폴라드 로를 구현한 코드다. 다만 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);
}
문제는 가 커지면, 모든 에 대해 를 찾는 일이 너무 부담스러워진다는 것이다(위 코드의 39번 줄). 이를 보다 최적화하기 위해, 인 경우만을 고려해도 됨을 보이도록 하자.
가 보다 작은, 1이 아닌 의 약수라 하자. 만약 이면 가 선택된 방식에 따라,
이다. 다시 말해 수열 에 를 적용한 수열은 언젠가 길이 의 구간이 무한히 반복되는 수열이 된다. 즉 만약 이면, 이다. 특히 가 보다 큰 의 배수이면, 가 되고, 따라서 인 정수 k가 있을 것이라 기대할 수 있다.
유일한 단점은 n의 자명하지 않은 인수인 가 처음으로 나타나는 때가 언제인지 바로 알 수 있는 것은 아니라는 점이다.
을 인수분해하려 한다고 하자. 이라 하면, 이로부터 만들 수 있는 수열은 다음과 같다.
각 와 를 비교해보면 다음을 얻을 수 있다.
이므로 113이 30623의 인수임을 알 수 있다.
한편 위 수열에 각각 을 해주면 다음의 새로운 수열을 얻을 수 있다.
이 수열에서는 이 계속해서 반복된다. 이를 그림으로 나타내면 다음과 같다.
이 모양은 그리스 문자 와 닮아 있다. 여기서 소개한 폴라드의 인수분해 알고리즘을 가리켜 "폴라드 로 알고리즘"이라 부르는 이유다.
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;
}
폴라드 로 알고리즘에서 인수분해할 은 "합성수인 홀수"여야 한다. 위 코드를 실행해보면 알겠지만 이 소수인 경우는 제대로 동작하지 않는다.
따라서 임의로 입력되는 이 합성수인지 아닌지도 모르는 경우에는 이를 판별할 방법도 있어야 한다. 임의의 큰 정수 의 소수 여부를 확인하기 위한 알고리즘으로는 다음으로 포스팅될 밀러-라빈 소수판별법(Miller-Rabin primality test)이 있고, 이 알고리즘을 폴라드 로와 조합해 임의의 큰 정수 의 소인수분해도 할 수 있다.