PS를 위한 수학 - Linear Sieve(오일러의 체)
Linear Sieve
Linear Sieve는 말 그대로 선형시간 $O(N)$에 소수 판별을 할 수 있는 알고리즘이다.
하지만 소수 판별 말고도 승법함수들을 $O(N)$에 덤으로 (?) 구하는것도 가능하기 때문에 쓸모가 많다. 아래 단락에서 살펴보자.
기존 에라토스테네스의 체는 $O(N \log\log N)$의 시간복잡도를 가지기 때문에 유의미한 시간복잡도 개선이 된다.
에라토스테네스의 체에서 합성수는 피치못하게 중복으로 검사가 된다.
$21=3 \cdot 7$ 이라는 수를 보면, $3$과 $7$모두 소수이기 때문에 $3,6,9,\dots$ 처럼 가는 과정과 $7,14,21,\dots$ 처럼 가는 과정에서 두 번 세진다.
따라서 $O(N)$의 시간복잡도가 나오지 못한다.
Linear Sieve는 이러한 문제를 해결한다.
원리
어떤 합성수 $x$를 $x=i \cdot p$ 라고 하자. $(p$는 소수이자 $x$의 최소 소인수$)$
$p$는 최소 소인수이기 때문에 $x=i \cdot p$에서 항상 $i \ge p$ 이다.
예를 들어, $21$의 최소 소인수 $p=3$ 이다.
어떤 합성수 $x$에 대해 $x$가 합성수다 라고 판단을 $x$의 최소 소인수인 $p$에서 단 한번만 이루어지게 하는 것이 핵심이다.
$x$에 집중하지 말고 $i$와 $p$에 집중한다.
$i$에 대해 반복문을 돌리며,
$(1)$ 현재 $i$가 아직 합성수라고 판단되지 않았다면 소수라고 판단한다.
$(2)$ 현재까지 검출된 소수 $p_j \le i$ 들에 대해서 작은 $p_j$ 부터, $i \cdot p_j$ 를 모두 살펴본다.
$x_j=i \cdot p_j$ 라 할 때, $x_j$의 최소 소인수는 $p_j$가 되는 것이다.
그런데 $x_j$가 이미 $i$에 대해 검사하기 전에 다른 $p < p_j$ 에 대해서 합성수라고 판단이 되어있을 수도 있지 않을까?
이런 상황을 방지하기 위해, $p_j \mid i$이면 $(2)$ 반복문을 종료한다.
$p_j \mid i$라는 것은 $p > p_j$ 인 $p$ 들에 대해서 $i \cdot p$의 최소 소인수가 $p$가 될 수 없기 때문이다.
$p_j \mid i$ 이기 때문에 $p_j \mid i \cdot p$ 이고 $p_j < p$ 라서 최소 소인수는 $p_j$ 이기 때문이다.
이런 과정을 거치면 의도대로 모든 합성수에 대해서 최소소인수 $p$들에 대해서 한 번씩만 검사가 되기 때문에 시간복잡도가 $O(N)$이 나온다.
구현
위 코드를 그대로 구현해보자.
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
}
$N=10,000,000$ 까지 검사했을 때, cnt
변수가 $16,034,519$이므로 거의 $O(N)$임을 볼 수 있다.
에라토스테네스의 체는 $29,465,738$ 회가 나왔다.
응용 1 - 소인수분해
$\sim N$ 까지의 수들에 대해 최소 소인수 배열을 구성한다면 빠르게 소인수분해를 할 수 있다.
왜냐면 최소 소인수들로 계속해서 나눠주기만 하면 되기 때문에 정확히 소수의 개수의 시간복잡도로 가능하다.
$O(1)$이라 보아도 무방
물론 Linear Sieve로만 최소 소인수 배열을 구성할 수 있는것은 아니다. 에라토스테네스의 체로도 가능하다.
void factorize(int x) {
while (x > 1) {
cout << min_prime_factor[x] << ' ';
x /= min_prime_factor[x];
}
}
응용 2 - 승법함수
승법함수 는 다음과 같은 성질을 만족하는 수론 함수이다.
$n \perp m$이면 $f(nm)=f(n) \cdot f(m)$
대표적으로 상수함수, $\tau$(약수의 개수), $\sigma$(약수의 합), $\mu$(뫼비우스 함수), $\phi$(오일러 파이 함수) 등이 있다.
Linear Sieve가 에라토스테네스의 체랑 다르게 유용한 경우가 이러한 승법함수를 구성하는 데에 있다.
정수 $x$에 대해 $f(x)$를 구성하는 데 있어,
$x$가 $1$이나 소수라면
대개 성질을 이용해 빠르게 $f(x)$를 채워줄 수 있다.
$x$가 합성수라면
Linear Sieve의 원리를 생각해보면 $x=i \cdot p$처럼 합성수에 대해 단 한번만 최소 소인수로써 어떤 연산을 해줄 수 있는 구조이고 $i$는 소수이든 합성수이든 함수값들이 이미 구해져있는 상태이다.
$i \perp p$ 라면 편하게 $f(x)=f(i)f(p)$ 처럼 해줄 수 있지만, $p \mid i$ 인 경우도 존재한다.
이런 경우들은 함수의 성질과 관계식에 따라 알잘딱하게 처리해주면 된다.
직접 예시를 보자.
위에서 말한 $\tau$(약수의 개수), $\sigma$(약수의 합), $\mu$(뫼비우스 함수), $\phi$(오일러 파이 함수) 를 모두 Linear Sieve로 $O(N)$에 구해보도록 하자.
구현
$x=1$
$0$에 대해 항등적이 아닌 승법 함수들은 $f(1)=1$ 이다.
즉, $\tau(1)=\sigma(1)=\mu(1)=\phi(1)=1$ 로 초기화한다.
$x$는 소수
$p$가 소수일 때 $f(p)$ 들은 다음과 같다.
$\tau(p)=2$ $\because$ $1$과 자기 자신
$\sigma(p)=p+1$ $\because$ $1$과 자기 자신의 합
$\mu(p)=-1$ $\because$ $(-1)^r,~~(r=1)$ 이여서
$\phi(p)=p-1$
$x$는 합성수
$x$가 합성수일 때는 $x=i \cdot p$ 라 할 때, $p \mid i$ 인 경우와 $p \nmid i$ 인 경우를 나누어서 생각하자.
$p \nmid i$
이 경우 $gcd(p,i)=1$ 이기 때문에 $f(x)=f(i)f(p)$ 로 처리해준다.
$p \mid i$
$x=p_1^{k_1}p_2^{k_2} \cdots p_r^{k_r}$ 라 하자.
$\tau(x)$
$\tau(x)=(k_1+1)(k_2+1)\cdots (k_r+1)$ 이기 때문에 현재 $x$에 $p=p_1$가 얼마나 곱해져있는지 알면 된다.
이를 $c$ 라고 하자.
$x$가 소수거나 $p \nmid x$라면 $c(x)=1$ 이고, 그렇지 않다면 $c(x)=c(i)+1$ 이 될 것이다.
따라서 $\tau(x)=\dfrac {\tau(i)}{c(i)+1} \cdot (c(i)+2)$
$\sigma(x)$
$\sigma(x)=\dfrac {p_1^{k_1+1}-1}{p_1-1} \cdot \dfrac {p_2^{k_2+1}-1}{p_2-1} \cdots$
따라서
구현할 땐 오버플로우에 주의해야한다.
$\mu(x)$
$\mu$함수는 제곱인수가 포함되면 $0$이기 때문에 그냥 $\mu(x)=0$ 이다.
$p^2$ 를 인수로 가지게 되기 때문이다.
$\phi(x)$
$\phi(x)=\displaystyle \prod_{i=1}^r p_i^{k_i-1}(p_i-1)$
즉, $\phi(x)=\phi(i) \cdot p$
코드
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