The Story of Joon
Linear-time construction of the sieve of Eratosthenes 본문
Linear-time construction of the sieve of Eratosthenes
jo_on 2022. 9. 11. 15:45에라토스테네스의 체는 소수를 구할 때 흔히 쓰는 방식이다. 구현도 쉽기 때문에 정수론과 관련된 PS 문제에서 자주 만날 수 있다.
일반적으로는 아래와 같이 구현한다.
vector<int> sieve_of_eratosthenes(int n) {
vector<bool> sieve(n + 1);
vector<int> prime;
for (int k = 2; k <= n; k++) {
if (sieve[k]) {
continue;
}
prime.push_back(k);
for (int i = 2; i * k <= n; i++) {
sieve[i * k] = true;
}
}
return prime;
}
이 알고리즘은 얼마나 효율적일까? 간단하게 각 정수를 평균적으로 몇 번 방문하는지 보면 된다. 이 방식은 n까지의 모든 양의 정수의 방문 횟수가 각 정수의 소인수 개수이다. 어떤 정수의 소인수 개수를 알려주는 함수로 prime omega function이라는 것이 있는데, n의 소인수 개수는 평균적으로 O(loglogn)개이다.
여기서 방법을 살짝 바꿔서 각 정수를 정확히 한 번만 방문하게 할 수 있다. 각 정수의 소인수 하나당 방문을 하는 대신 최소 소인수에 대해서만 방문을 하는 것이다. 아이디어는 안쪽 루프에서 현재 보고 있는 메인 루프의 정수 k의 배수를 방문하되 각 소수 p에 대해 pk꼴의 수만 방문하고, 작은 p부터 방문하면서 k가 p의 배수가 되면 멈추는 것이다. 이렇게 하면 p가 pk의 최소 소인수일 때만 방문을 하게 되므로 우리의 목적을 달성할 수 있다.
주의할 점은 일반적인 에라토스테네스의 체와 방문하는 방식이 다르기 때문에, k가 합성수일 때도 방문을 해주어야 한다.
vector<int> sieve_of_eratosthenes(int n) {
vector<bool> sieve(n + 1);
vector<int> prime;
for (int k = 2; k <= n; k++) {
if (!sieve[k]) {
prime.push_back(k);
}
for (int i = 0; prime[i] * k <= n; i++) {
int p = prime[i];
sieve[p * k] = true;
if (k % p == 0) {
break;
}
}
}
return prime;
}
냉정히 말해서 이렇게 한다 해도 동작 시간을 크게 줄여주지는 못한다. 기존 체에서 평균 방문 횟수인 loglogn이 너무나도 작은 값이기 때문이다. 하지만 이 방식이 기존 방식과 차별화되는 가장 큰 특징은 각 정수 n을 정확히 한 번 방문하고, 방문할 때 n의 최소 소인수를 즉시 알 수 있으며 n을 방문하기 전 n의 약수를 모두 방문한다는 것이다. (마지막 사실은 증명하지는 않았으나 귀납법으로 쉽게 확인 가능하므로 생략한다.) 이 특징 때문에 특정 정수 이하의 모든 양의 정수에 대해 곱셈 함수의 값을 구하는 데 유용하다.
어떤 산술 함수 f:Z+→C가 f(1)=1이고 서로소인 두 양의 정수 a와 b에 대해 f(ab)=f(a)f(b)를 만족하면 곱셈 함수라고 한다. 곱셈 함수의 특징은 모든 소수 p와 양의 정수 ℓ에 대해 f(pℓ)의 값이 정해지면 함수가 유일하게 결정된다는 것이다.
이를 이용하면 dynamic programming으로 곱셈 함수를 계산할 수 있다. 어떤 양의 정수 n의 최소 소인수 p와 p로 나누어 떨어지지 않는 m에 대해 n=pℓm으로 나타날 때, f(n)=f(pℓ)f(m)이기 때문이다. 우리는 p의 값만 알고 pℓ의 값은 모르지 않느냐고 생각할 수 있지만, 이 값 역시 (곱셈 함수는 아니지만) dynamic programming으로 계산할 수 있다.
대표적인 곱셈 함수인 Euler's totient function의 값을 구하는 코드를 마지막으로 글을 마친다. 위키피디아 링크에서 더 다양한 곱셈 함수에 대해 알아볼 수 있다.
vector<int> euler_totient(int n) {
vector<bool> sieve(n + 1);
vector<int> prime;
vector<int> phi(n + 1);
phi[1] = 1;
for (int k = 2; k <= n; k++) {
if (!sieve[k]) {
prime.push_back(k);
phi[k] = k - 1;
}
for (int i = 0; prime[i] * k <= n; i++) {
int p = prime[i];
sieve[p * k] = true;
if (k % p == 0) {
phi[p * k] = phi[k] * p;
break;
} else {
phi[p * k] = phi[k] * (p - 1);
}
}
}
return phi;
}
'Computer Science > 알고리즘' 카테고리의 다른 글
Linear Algebra in Problem Solving (3) (1) | 2022.12.31 |
---|---|
Linear Algebra in Problem Solving (2) (2) | 2022.09.02 |
Linear Algebra in Problem Solving (1) (0) | 2022.08.31 |
2015 ACM-ICPC 한국 예선 F - 파일 합치기 (2) | 2020.05.02 |
Link/Cut Tree (2) (7) | 2017.08.21 |