PS를 위한 수학 - Linear Sieve(오일러의 체)
Linear SievePermalink
Linear Sieve는 말 그대로 선형시간 에 소수 판별을 할 수 있는 알고리즘이다.
하지만 소수 판별 말고도 승법함수들을 에 덤으로 (?) 구하는것도 가능하기 때문에 쓸모가 많다. 아래 단락에서 살펴보자.
기존 에라토스테네스의 체는 의 시간복잡도를 가지기 때문에 유의미한 시간복잡도 개선이 된다.
에라토스테네스의 체에서 합성수는 피치못하게 중복으로 검사가 된다.
이라는 수를 보면, 과 모두 소수이기 때문에 처럼 가는 과정과 처럼 가는 과정에서 두 번 세진다.
따라서 의 시간복잡도가 나오지 못한다.
Linear Sieve는 이러한 문제를 해결한다.
원리Permalink
어떤 합성수 를 라고 하자. 는 소수이자 의 최소 소인수
는 최소 소인수이기 때문에 에서 항상 이다.
예를 들어, 의 최소 소인수 이다.
어떤 합성수 에 대해 가 합성수다 라고 판단을 의 최소 소인수인 에서 단 한번만 이루어지게 하는 것이 핵심이다.
에 집중하지 말고 와 에 집중한다.
에 대해 반복문을 돌리며,
현재 가 아직 합성수라고 판단되지 않았다면 소수라고 판단한다.
현재까지 검출된 소수 들에 대해서 작은 부터, 를 모두 살펴본다.
라 할 때, 의 최소 소인수는 가 되는 것이다.
그런데 가 이미 에 대해 검사하기 전에 다른 에 대해서 합성수라고 판단이 되어있을 수도 있지 않을까?
이런 상황을 방지하기 위해, 이면 반복문을 종료한다.
라는 것은 인 들에 대해서 의 최소 소인수가 가 될 수 없기 때문이다.
이기 때문에 이고 라서 최소 소인수는 이기 때문이다.
이런 과정을 거치면 의도대로 모든 합성수에 대해서 최소소인수 들에 대해서 한 번씩만 검사가 되기 때문에 시간복잡도가 이 나온다.
구현Permalink
위 코드를 그대로 구현해보자.
const int N = 10000000;
int min_prime_factor[N + 1];
vector<int> primes;
int cnt = 0; // debug count
void linear_sieve() {
memset(min_prime_factor, -1, sizeof min_prime_factor);
for (int i = 2; i <= N; i++) {
if (min_prime_factor[i] == -1) {
min_prime_factor[i] = i;
primes.push_back(i);
}
for (int p: primes) {
cnt++; // debug count
ll x = i * p;
if (x > N) break;
min_prime_factor[x] = p;
if (i % p == 0) break;
}
}
cout << cnt; // 16034519
}
까지 검사했을 때, cnt
변수가 이므로 거의 임을 볼 수 있다.
에라토스테네스의 체는 회가 나왔다.
응용 1 - 소인수분해Permalink
까지의 수들에 대해 최소 소인수 배열을 구성한다면 빠르게 소인수분해를 할 수 있다.
왜냐면 최소 소인수들로 계속해서 나눠주기만 하면 되기 때문에 정확히 소수의 개수의 시간복잡도로 가능하다.
이라 보아도 무방
물론 Linear Sieve로만 최소 소인수 배열을 구성할 수 있는것은 아니다. 에라토스테네스의 체로도 가능하다.
void factorize(int x) {
while (x > 1) {
cout << min_prime_factor[x] << ' ';
x /= min_prime_factor[x];
}
}
응용 2 - 승법함수Permalink
승법함수 는 다음과 같은 성질을 만족하는 수론 함수이다.
이면
대표적으로 상수함수, (약수의 개수), (약수의 합), (뫼비우스 함수), (오일러 파이 함수) 등이 있다.
Linear Sieve가 에라토스테네스의 체랑 다르게 유용한 경우가 이러한 승법함수를 구성하는 데에 있다.
정수 에 대해 를 구성하는 데 있어,
가 이나 소수라면
대개 성질을 이용해 빠르게 를 채워줄 수 있다.
가 합성수라면
Linear Sieve의 원리를 생각해보면 처럼 합성수에 대해 단 한번만 최소 소인수로써 어떤 연산을 해줄 수 있는 구조이고 는 소수이든 합성수이든 함수값들이 이미 구해져있는 상태이다.
라면 편하게 처럼 해줄 수 있지만, 인 경우도 존재한다.
이런 경우들은 함수의 성질과 관계식에 따라 알잘딱하게 처리해주면 된다.
직접 예시를 보자.
위에서 말한 (약수의 개수), (약수의 합), (뫼비우스 함수), (오일러 파이 함수) 를 모두 Linear Sieve로 에 구해보도록 하자.
구현Permalink
Permalink
에 대해 항등적이 아닌 승법 함수들은 이다.
즉, 로 초기화한다.
는 소수Permalink
가 소수일 때 들은 다음과 같다.
과 자기 자신
과 자기 자신의 합
이여서
는 합성수Permalink
가 합성수일 때는 라 할 때, 인 경우와 인 경우를 나누어서 생각하자.
Permalink
이 경우 이기 때문에 로 처리해준다.
Permalink
라 하자.
이기 때문에 현재 에 가 얼마나 곱해져있는지 알면 된다.
이를 라고 하자.
가 소수거나 라면 이고, 그렇지 않다면 이 될 것이다.
따라서
따라서
구현할 땐 오버플로우에 주의해야한다.
함수는 제곱인수가 포함되면 이기 때문에 그냥 이다.
를 인수로 가지게 되기 때문이다.
즉,
코드Permalink
const int N = 10000000;
int min_prime_factor[N + 1], tau[N + 1], sigma[N + 1], mu[N + 1], phi[N + 1], c[N + 1];
vector<int> primes;
inline ll poww(ll a, ll b) {
ll ret = 1;
while (b) {
if (b & 1) {
ret *= a;
}
a *= a;
b >>= 1;
}
return ret;
}
void linear_sieve() {
memset(min_prime_factor, -1, sizeof min_prime_factor);
tau[1] = sigma[1] = mu[1] = phi[1] = 1;
for (int i = 2; i <= N; i++) {
if (min_prime_factor[i] == -1) {
c[i] = 1;
min_prime_factor[i] = i;
primes.push_back(i);
tau[i] = 2; // 소수의 약수의 개수: 1과 자기 자신
sigma[i] = i + 1; // 소수의 약수의 합: 1 + p
mu[i] = -1; // 소수의 서로 다른 소인수 개수: (-1)^1
phi[i] = i - 1; // 소수의 자신 이하에서 자신과 서로소인 개수: p - 1
}
for (int p: primes) {
ll x = i * p;
if (x > N) break;
min_prime_factor[x] = p;
if (i % p == 0) {
c[x] = c[i] + 1;
tau[x] = tau[i] * (c[i] + 2) / (c[i] + 1);
sigma[x] = sigma[i] * (poww(p, c[i] + 2) - 1) / (poww(p, c[i] + 1) - 1);
mu[x] = 0;
phi[x] = phi[i] * p;
break;
}
c[x] = 1;
tau[x] = tau[i] * tau[p];
sigma[x] = sigma[i] * sigma[p];
mu[x] = mu[i] * mu[p];
phi[x] = phi[i] * phi[p];
}
}
}
Comments