2022. 9. 18. 16:33ㆍ수학
이 알고리즘은 에라토스테네스의 체를 더 효율적으로 돌리기 위한 알고리즘으로 다음과 같은 점화식을 이용한다.
dp[v][m]=dp[v][m−1]−(dp[⌊vm⌋][m−1]−dp[m−1][m−1])
여기서 dp[v][m]은 m까지의 수로 체를 돌렸을 때 1부터 v까지 수 중 소수로 간주되는 것이 몇 개인지 말하고 있다. 우리가 m−1까지의 수를 보았다고 하자. 만약 m이 소수가 아니라면 체를 돌릴 필요가 없으니 dp[v][m]=dp[v][m−1]이다. 또 m2이 v보다 크다면 역시 체를 돌릴 필요가 없어 dp[v][m]=dp[v][m−1]이다. 그럼 m2<v이며 m이 소수인 경우를 보자. dp[v][m−1]에서 제외해야 할 수는 m의 배수들임이 자명하다. 더 나아가 p가 소수라고 간주되는 것들이고 m⋅p 꼴이어여 한다. 왜냐면 소수라 간주되지 않는 수들은 이미 앞에서 걸러졌기 때문이다. 또, p>m−1이어야 한다. 이것은 소수이지만 이미 앞에서 걸렀다. 따라서 p>m−1이고 p가 소수인 개수를 세야 한다. dp[⌊vm][m−1]은 p 중 소수인 것들을 가지고 있다. 여기서 dp[m−1][m−1] 즉, m−1까지 소수의 개수를 뺀다면 p>m−1인 소수 p의 개수를 알 수 있다. 따라서 저러한 식이 유도되는 것이다.
저 점화식에 dp를 적용하기 위해서는 총 2√N개의 값이 필요하다. 먼저 m의 가능한 값들을 생각하자. v는 최대 N일 것인데 m2<v이니 m은 √N까지 만 생각하면 된다. 따라서 첫 번째 인자로 생각할 수 있는 것은 1,⋯,√N과 ⌊N1⌋,⋯,⌊N√N⌋ 이다.
이제 2개의 배열을 만들고 하나는 1,⋯,√N 부분을 다른 하나는 ⌊N1⌋,⋯,⌊N√N⌋ 부분을 관리하게 하자. m을 1부터 √N까지 증가시키는 방향으로 구현한다. 각 m마다 새롭게 dp[v][m]을 갱신하는 경우는 v가 큰 것 먼저 값을 바꿔야 데이터 손실이 없다. dp[m−1][m−1] 부분은 사실 m−1이하의 소수의 개수를 의미하기 때문에 미리 √N까지 에라토스테네스의 체를 돌려 전처리를 해 두자.
아래에는 구현한 코드가 있다.
보려면 클릭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
#include<stdio.h>
#include<math.h>
#define MAX 333333
typedef long long ll;
ll num_pr[MAX], A[MAX], B[MAX];
// A는 dp[1,2,...\sqrt{n}]을 관리, B는 dp[N/1, N/2,...,\sqrt{n}]을 관리.
// B는 항상 \sqrt{n}이상인 것을 포함한다.
// A, B의 초기 값은 dp[v][1] 인데 1을 탐색했다고 보면 dp[v][1]=v-1이다.
// dp[v][m] 에서 만약 m ^ 2 > v 거나 m이 소수가 아니면 더 이상 진전 될 것이 없다. 그래서 m을 \sqrt{N} 까지 돌린다. v의 최댓값이 N이기 때문.
// top-down으로 관리해야 데이터 손실이 없다. B먼저, 그 다음 A.
// i = m 이고 j = v 라고 생각하면 편하다.
ll ispr[MAX];
ll N, C;
ll get_val(ll x){
if(x <= C) return A[x];
return B[N / x];
}
void calc_primes(){
ll i, j;
for(i = 1;i <= C;i++) A[i] = i - 1;
for(i = 1;i <= C;i++) B[i] = N / i - 1;
for(i = 2;i <= C;i++){
if(ispr[i]) continue;
for(j = 1;j <= C;j++){
if(N / j < i * i) break;
B[j] = B[j] - (get_val(N / j / i) - num_pr[i - 1]);
}
for(j = C;j >= 0;j--){
if(j < i * i) break;
A[j] = A[j] - (get_val(j / i) - num_pr[i - 1]);
}
}
}
int main(){
ll i, j;
N = 1e11; C = sqrt(N);
ispr[1] = 1;
for(i = 2;i <= C;i += 2){
if(ispr[i]) continue;
for(j = i * i;j <= C;j += i){
ispr[j] = 1;
}
if(i == 2) i = 1;
}
for(i = 2;i <= C;i++){
num_pr[i] = num_pr[i - 1];
if(ispr[i]) continue;
num_pr[i]++;
}
calc_primes();
printf("%lld", B[1]);
}
|
cs |
이를 응용한 예시로 4n+1꼴 소수의 개수를 구하는 프로그램을 만들 수 있다.
알고리즘의 핵심 원리는 모든 4n+3 꼴 수를 보존하고 나중에 한꺼번에 제외하는 것이다.
아래의 dp식을 보자.
dp4[v][m]=\begin{cases} dp4[v][m-1] - (dp4[\lfloor\frac{v}{m}\rfloor][m-1]-dp1[m-1][m-1]-(\lfloor\frac{v}{m}\rfloor-3)/4-1), &m\equiv 1\pmod{4}\\ dp4[v][m-1] - ((dp[\lfloor\frac{v}{m}\rfloor][m-1] - dp4[\lfloor\frac{v}{m}\rfloor][m-1] + (\lfloor\frac{v}{m}\rfloor-3)/4+1)-(dp[m-1][m-1]-dp1[m-1][m-1])), &m\equiv 3\pmod{4}\end{cases}
이때 dp4는 에라토스테네스의 체를 m까지 보았을 때 남아있는 v이하의 4n+1꼴 소수들과 모든 4n+3꼴 소수들의 개수이다. dp1은 m-1까지 4n+1꼴 소수의 개수이다.
m이 4n+1 꼴 소수인 경우를 생각하자. 같은 원리로 m\cdot p 꼴 수를 걸러내야 하는데 4n+3꼴은 반드시 보존해야 하므로 p가 4n+1꼴 소수인 것만 생각한다. dp4[\lfloor\frac{v}{m}\rfloor][m-1]가 \lfloor\frac{v}{m}\rfloor 까지의 4n+3꼴 수들을 포함하므로 정확히 4n+1꼴 수들만 모으기 위해 -(\lfloor\frac{v}{m}\rfloor-3)/4-1항을 추가한 것이다.
m이 4n+3 꼴 소수인 경우를 생각하자. 이번에도 m\cdot p 꼴 수를 거르는데 4n+1 꼴 합성수를 걸러야 겠다. 그럼 p가 4n+3 꼴 소수이므로 dp[\lfloor\frac{v}{m}\rfloor][m-1] 즉, m-1까지 보았을 때 소수라고 간주되는 것들 중에서 dp4[\lfloor\frac{v}{m}\rfloor][m-1]의 4n+1꼴 소수라고 간주되는 것들을 빼면 4n+3꼴 소수라고 간주되는 것들의 개수를 얻을 수 있다. p가 m-1 초과이므로 그 뒤의 항이 추가된 것이다.
이 알고리즘은 소수의 개수를 관리함과 동시에 4n+1꼴 소수의 개수도 관리하여 시간이 더 오래 걸린다. 더 효율적인 방법이 있으면 알려주길 바란다!^^ 아래에는 구현한 코드가 있다.
보려면 클릭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
|
#include<stdio.h>
#include<math.h>
#define MAX 333333
typedef long long ll;
ll num_pr[MAX], A[MAX], B[MAX];
ll A4[MAX], B4[MAX];
// A는 dp[1,2,...\sqrt{n}]을 관리, B는 dp[N/1, N/2,...,\sqrt{n}]을 관리.
// B는 항상 \sqrt{n}이상인 것을 포함한다.
// A, B의 초기 값은 dp[v][1] 인데 1을 탐색했다고 보면 dp[v][1]=v-1이다.
// dp[v][m] 에서 만약 m ^ 2 > v 거나 m이 소수가 아니면 더 이상 진전 될 것이 없다. 그래서 m을 \sqrt{N} 까지 돌린다. v의 최댓값이 N이기 때문.
// top-down으로 관리해야 데이터 손실이 없다. B먼저, 그 다음 A.
// i = m 이고 j = v 라고 생각하면 편하다.
ll ispr[MAX];
ll num_pr1[MAX];
ll N, C;
ll get_val_4(ll x){
if(x <= C) return A4[x];
return B4[N / x];
}
ll get_val(ll x){
if(x <= C) return A[x];
return B[N / x];
}
//4n+3을 보존하는 방식
//A4 또는 B4는 주어진 수 v이하의 모든 4n+3꼴 수와 4n+1 꼴 semi 소수(에라토스테네스의 체를 m까지 돌렸을 때 소수라 판정된 수)를 포함한다.
//목표는 결과 가 나올 때까지 4n+3꼴 모든 수를 보존하는 것. 이후 한번에 제거하는 것이다.
void calc_primes(){
ll i, j;
for(i = 1;i <= C;i++) A[i] = i - 1 - i / 2 + 1;
for(i = 1;i <= C;i++) B[i] = N / i - 1 - N / i / 2 + 1; //2 배수 미리 처리함.
for(i = 1;i <= C;i++) A4[i] = i - 1 - i / 2;
for(i = 1;i <= C;i++) B4[i] = N / i - 1 - N / i / 2;
for(i = 3;i <= C;i++){
if(ispr[i]) continue;
for(j = 1;j <= C;j++){
if(N / j < i * i) break;
if(i % 4 == 1) B4[j] = B4[j] - (get_val_4(N / j / i) - num_pr1[i - 1] - (N / j / i - 3) / 4 - 1);
//(4n+1)*(4n+1)꼴 합성수를 제외하는 과정이다.
//N/j/i까지에는 4n+3꼴의 모든 수와 4n+1꼴 소수가 들어 있다. num_pr1[i-1]은 i-1까지 4n+1꼴 소수가 들어 있다.
//따라서 N/j/i까지 4n+3꼴 수들을 빼면 i-1보다 큰 4n+1꼴 소수들을 얻을 수 있다.
else B4[j] = B4[j] - ((get_val(N / j / i) - get_val_4(N / j / i) + (N / j / i - 3) / 4 + 1) - (num_pr[i - 1] - num_pr1[i - 1]));
// (4n+3)*(4n+3) 꼴 합성수를 제외하는 과정이다.
//N/j/i까지 소수들에서 N/j/i까지 4n+1꼴 소수들을 뺴면 4n+3꼴 소수들만 얻을 수 있다. get_val_4(N/j/i)에는 4n+3꼴 수들도 있으니 제외해주자.
//num_pr1[i-1]은 i-1까지 4n+1꼴 소수들만 모았다.
//****주의 사항 : get_val_4(N / j / i)에서 아직 남아있는 4n+1꼴 합성수가 있다. 하지만 get_val(N / j / i)에도 남아 있는 합성수가 있기 때문에 상쇄된다.
// 이것이 A,B와 A4,B4를 같이 iterating 하는 이유이다.
}
for(j = C;j >= 1;j--){
if(j < i * i) break;
if(i % 4 == 1) A4[j] = A4[j] - (get_val_4(j / i) - num_pr1[i - 1] - (j / i - 3) / 4 - 1);
else A4[j] = A4[j] - ((get_val(j / i) - get_val_4(j / i) + (j / i - 3) / 4 + 1) - (num_pr[i - 1] - num_pr1[i - 1]));
}
for(j = 1;j <= C;j++){
if(N / j < i * i) break;
B[j] = B[j] - (get_val(N / j / i) - num_pr[i - 1]);
}
for(j = C;j >= 1;j--){
if(j < i * i) break;
A[j] = A[j] - (get_val(j / i) - num_pr[i - 1]);
}
}
}
int main(){
ll i, j;
scanf("%lld", &N);
C = sqrt(N);
ispr[1] = 1;
for(i = 2;i <= C;i += 2){
if(ispr[i]) continue;
for(j = i * i;j <= C;j += i){
ispr[j] = 1;
}
if(i == 2) i = 1;
}
for(i = 2;i <= C;i++){
num_pr[i] = num_pr[i - 1];
num_pr1[i] = num_pr1[i - 1];
if(ispr[i]) continue;
num_pr[i]++;
if(i % 4 == 1) num_pr1[i]++;
}
calc_primes();
printf("%lld", B4[1] - (N - 3) / 4 - 1);
}
|
cs |
'수학' 카테고리의 다른 글
FFT(고속 푸리에 변환) (0) | 2023.02.25 |
---|---|
Funny fact in Linear Algebra (2) | 2022.11.02 |
Extension of Wilson's Theorem and its application (12) | 2022.07.14 |
Modulo Prime Power Trick (0) | 2022.01.28 |
Mobius Inversion (0) | 2022.01.23 |