Linear SievePermalink

Linear Sieve는 말 그대로 선형시간 O(N)O(N)에 소수 판별을 할 수 있는 알고리즘이다.

하지만 소수 판별 말고도 승법함수들을 O(N)O(N)에 덤으로 (?) 구하는것도 가능하기 때문에 쓸모가 많다. 아래 단락에서 살펴보자.

기존 에라토스테네스의 체는 O(NloglogN)O(N \log\log N)의 시간복잡도를 가지기 때문에 유의미한 시간복잡도 개선이 된다.

에라토스테네스의 체에서 합성수는 피치못하게 중복으로 검사가 된다.

21=3721=3 \cdot 7 이라는 수를 보면, 3377모두 소수이기 때문에 3,6,9,3,6,9,\dots 처럼 가는 과정과 7,14,21,7,14,21,\dots 처럼 가는 과정에서 두 번 세진다.

따라서 O(N)O(N)의 시간복잡도가 나오지 못한다.

Linear Sieve는 이러한 문제를 해결한다.

원리Permalink

어떤 합성수 xxx=ipx=i \cdot p 라고 하자. (p(p는 소수이자 xx의 최소 소인수))

pp는 최소 소인수이기 때문에 x=ipx=i \cdot p에서 항상 ipi \ge p 이다.

예를 들어, 2121의 최소 소인수 p=3p=3 이다.

어떤 합성수 xx에 대해 xx가 합성수다 라고 판단을 xx의 최소 소인수인 pp에서 단 한번만 이루어지게 하는 것이 핵심이다.

xx에 집중하지 말고 iipp에 집중한다.

ii에 대해 반복문을 돌리며,

(1)(1) 현재 ii가 아직 합성수라고 판단되지 않았다면 소수라고 판단한다.

(2)(2) 현재까지 검출된 소수 pjip_j \le i 들에 대해서 작은 pjp_j 부터, ipji \cdot p_j 를 모두 살펴본다.

xj=ipjx_j=i \cdot p_j 라 할 때, xjx_j의 최소 소인수는 pjp_j가 되는 것이다.

그런데 xjx_j가 이미 ii에 대해 검사하기 전에 다른 p<pjp < p_j 에 대해서 합성수라고 판단이 되어있을 수도 있지 않을까?

이런 상황을 방지하기 위해, pjip_j \mid i이면 (2)(2) 반복문을 종료한다.

pjip_j \mid i라는 것은 p>pjp > p_jpp 들에 대해서 ipi \cdot p의 최소 소인수가 pp가 될 수 없기 때문이다.

pjip_j \mid i 이기 때문에 pjipp_j \mid i \cdot p 이고 pj<pp_j < p 라서 최소 소인수는 pjp_j 이기 때문이다.

이런 과정을 거치면 의도대로 모든 합성수에 대해서 최소소인수 pp들에 대해서 한 번씩만 검사가 되기 때문에 시간복잡도가 O(N)O(N)이 나온다.

구현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  
}

N=10,000,000N=10,000,000 까지 검사했을 때, cnt 변수가 16,034,51916,034,519이므로 거의 O(N)O(N)임을 볼 수 있다.

에라토스테네스의 체는 29,465,73829,465,738 회가 나왔다.

응용 1 - 소인수분해Permalink

N\sim N 까지의 수들에 대해 최소 소인수 배열을 구성한다면 빠르게 소인수분해를 할 수 있다.

왜냐면 최소 소인수들로 계속해서 나눠주기만 하면 되기 때문에 정확히 소수의 개수의 시간복잡도로 가능하다.

O(1)O(1)이라 보아도 무방

물론 Linear Sieve로만 최소 소인수 배열을 구성할 수 있는것은 아니다. 에라토스테네스의 체로도 가능하다.

void factorize(int x) {  
   while (x > 1) {  
      cout << min_prime_factor[x] << ' ';  
      x /= min_prime_factor[x];  
   }  
}

응용 2 - 승법함수Permalink

승법함수 는 다음과 같은 성질을 만족하는 수론 함수이다.

nmn \perp m이면 f(nm)=f(n)f(m)f(nm)=f(n) \cdot f(m)

대표적으로 상수함수, τ\tau(약수의 개수), σ\sigma(약수의 합), μ\mu(뫼비우스 함수), ϕ\phi(오일러 파이 함수) 등이 있다.

Linear Sieve가 에라토스테네스의 체랑 다르게 유용한 경우가 이러한 승법함수를 구성하는 데에 있다.

정수 xx에 대해 f(x)f(x)를 구성하는 데 있어,

xx11이나 소수라면

대개 성질을 이용해 빠르게 f(x)f(x)를 채워줄 수 있다.

xx가 합성수라면

Linear Sieve의 원리를 생각해보면 x=ipx=i \cdot p처럼 합성수에 대해 단 한번만 최소 소인수로써 어떤 연산을 해줄 수 있는 구조이고 ii는 소수이든 합성수이든 함수값들이 이미 구해져있는 상태이다.

ipi \perp p 라면 편하게 f(x)=f(i)f(p)f(x)=f(i)f(p) 처럼 해줄 수 있지만, pip \mid i 인 경우도 존재한다.

이런 경우들은 함수의 성질과 관계식에 따라 알잘딱하게 처리해주면 된다.

직접 예시를 보자.

위에서 말한 τ\tau(약수의 개수), σ\sigma(약수의 합), μ\mu(뫼비우스 함수), ϕ\phi(오일러 파이 함수) 를 모두 Linear Sieve로 O(N)O(N)에 구해보도록 하자.

구현Permalink

x=1x=1Permalink

00에 대해 항등적이 아닌 승법 함수들은 f(1)=1f(1)=1 이다.

즉, τ(1)=σ(1)=μ(1)=ϕ(1)=1\tau(1)=\sigma(1)=\mu(1)=\phi(1)=1 로 초기화한다.

xx는 소수Permalink

pp가 소수일 때 f(p)f(p) 들은 다음과 같다.

τ(p)=2\tau(p)=2 \because 11과 자기 자신

σ(p)=p+1\sigma(p)=p+1 \because 11과 자기 자신의 합

μ(p)=1\mu(p)=-1 \because (1)r,  (r=1)(-1)^r,~~(r=1) 이여서

ϕ(p)=p1\phi(p)=p-1

xx는 합성수Permalink

xx가 합성수일 때는 x=ipx=i \cdot p 라 할 때, pip \mid i 인 경우와 pip \nmid i 인 경우를 나누어서 생각하자.

pip \nmid iPermalink

이 경우 gcd(p,i)=1gcd(p,i)=1 이기 때문에 f(x)=f(i)f(p)f(x)=f(i)f(p) 로 처리해준다.

pip \mid iPermalink

x=p1k1p2k2prkrx=p_1^{k_1}p_2^{k_2} \cdots p_r^{k_r} 라 하자.

τ(x)\tau(x)

τ(x)=(k1+1)(k2+1)(kr+1)\tau(x)=(k_1+1)(k_2+1)\cdots (k_r+1) 이기 때문에 현재 xxp=p1p=p_1가 얼마나 곱해져있는지 알면 된다.

이를 cc 라고 하자.

xx가 소수거나 pxp \nmid x라면 c(x)=1c(x)=1 이고, 그렇지 않다면 c(x)=c(i)+1c(x)=c(i)+1 이 될 것이다.

따라서 τ(x)=τ(i)c(i)+1(c(i)+2)\tau(x)=\dfrac {\tau(i)}{c(i)+1} \cdot (c(i)+2)

σ(x)\sigma(x)

σ(x)=p1k1+11p11p2k2+11p21\sigma(x)=\dfrac {p_1^{k_1+1}-1}{p_1-1} \cdot \dfrac {p_2^{k_2+1}-1}{p_2-1} \cdots

따라서

σ(x)=σ(i)p1p1c(i)+11pc(i)+21p1=σ(i)pc(i)+21pc(i)+11 \begin{aligned} \sigma(x)&=\sigma(i) \cdot \dfrac {p-1}{p_1^{c(i)+1}-1} \cdot \dfrac {p^{c(i)+2}-1}{p-1}\\ &=\sigma(i) \cdot \dfrac {p^{c(i)+2}-1}{p^{c(i)+1}-1} \end{aligned}

구현할 땐 오버플로우에 주의해야한다.

μ(x)\mu(x)

μ\mu함수는 제곱인수가 포함되면 00이기 때문에 그냥 μ(x)=0\mu(x)=0 이다.

p2p^2 를 인수로 가지게 되기 때문이다.

ϕ(x)\phi(x)

ϕ(x)=i=1rpiki1(pi1)\phi(x)=\displaystyle \prod_{i=1}^r p_i^{k_i-1}(p_i-1)

즉, ϕ(x)=ϕ(i)p\phi(x)=\phi(i) \cdot p

코드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