문제 소개
이 튜토리얼에서는 연속된 두 수는 서로소(coprime)가 아니면서, 연속된 세 수는 반드시 서로소가 되도록 수열을 출력하는 프로그램을 C++로 구현하는 방법을 알아봅니다.
문제를 정리하면 다음과 같습니다. 정수 N이 주어졌을 때, 10억(109)보다 작은 N개의 정수를 출력해야 하며, 출력 결과는 아래 두 조건을 동시에 만족해야 합니다.
조건 1. 이웃한 두 수의 최대공약수(GCD)는 1이 아니어야 합니다. 즉, 인접한 두 수는 반드시 공약수를 가져야 합니다.
조건 2. 이웃한 세 수의 최대공약수는 1이어야 합니다. 즉, 연속된 세 수를 모두 나눌 수 있는 공약수는 존재하지 않아야 합니다.
예를 들어 N이 4라고 가정해 보겠습니다. 이때 두 조건을 모두 충족하는 수열은 다음과 같습니다.
6 15 35 14
실제로 gcd(6, 15) = 3, gcd(15, 35) = 5, gcd(35, 14) = 7이므로 이웃한 두 수는 서로소가 아니며, 반면 gcd(6, 15, 35) = 1, gcd(15, 35, 14) = 1이므로 연속된 세 수는 서로소임을 확인할 수 있습니다.
접근 방법
이 문제의 핵심 아이디어는 소수 배열에서 인접한 두 소수의 곱을 수열의 원소로 사용하는 것입니다. 수열의 i번째 원소를 p[i] × p[i+1](여기서 p는 소수 배열)로 정의하면 두 조건이 자연스럽게 성립합니다.
- ar[i] = p[i] × p[i+1]과 ar[i+1] = p[i+1] × p[i+2]는 공통 인수 p[i+1]을 공유하므로, 두 수의 GCD는 절대 1이 될 수 없습니다.
- 연속된 세 수 ar[i], ar[i+1], ar[i+2]를 모두 나누는 소수는 존재하지 않으므로, 세 수의 GCD는 항상 1입니다.
전체 구현 과정은 다음과 같이 진행됩니다.
1. 에라토스테네스의 체(Sieve of Eratosthenes)를 이용해 2,000,000 이하의 모든 소수를 미리 구합니다.
2. 인접한 두 소수의 곱이 10억을 넘지 않는 범위까지만 후보 수열을 생성하고, 이미 사용된 곱은 map 자료구조로 중복 여부를 관리합니다.
3. 요청된 개수 N만큼의 원소를 출력하되, 마지막 원소는 마지막 두 원소의 GCD에 2를 곱한 값으로 출력해 조건을 그대로 유지합니다.
C++ 예제 코드
#include <bits/stdc++.h>
using namespace std;
#define limit 1000000000
#define MAX_PRIME 2000000
#define MAX 1000000
#define I_MAX 50000
map<int, int> map1;
int b[MAX];
int p[MAX];
int j = 0;
bool prime[MAX_PRIME + 1];
void sieve(int n){
memset(prime, true, sizeof(prime));
for (int p = 2; p * p <= n; p++){
if (prime[p] == true){
for (int i = p * p; i <= n; i += p)
prime[i] = false;
}
}
for (int p = 2; p <= n; p++){
if (prime[p]) {
b[j++] = p;
}
}
}
int gcdiv(int a, int b){
if (b == 0)
return a;
return gcdiv(b, a % b);
}
//필요한 수열을 출력하는 함수
void print_elements(int n){
sieve(MAX_PRIME);
int i, g, k, l, m, d;
int ar[I_MAX + 2];
for (i = 0; i < j; i++){
if ((b[i] * b[i + 1]) > limit)
break;
p[i] = b[i];
map1[b[i] * b[i + 1]] = 1;
}
d = 550;
bool flag = false;
for (k = 2; (k < d - 1) && !flag; k++){
for (m = 2; (m < d) && !flag; m++){
for (l = m + k; l < d; l += k){
if (((b[l] * b[l + k]) < limit)
&& (l + k) < d && p[i - 1] != b[l + k]
&& p[i - 1] != b[l] && map1[b[l] * b[l + k]] != 1){
if (map1[p[i - 1] * b[l]] != 1){
p[i] = b[l];
map1[p[i - 1] * b[l]] = 1;
i++;
}
}
if (i >= I_MAX) {
flag = true;
break;
}
}
}
}
for (i = 0; i < n; i++)
ar[i] = p[i] * p[i + 1];
for (i = 0; i < n - 1; i++)
cout << ar[i] << " ";
g = gcdiv(ar[n - 1], ar[n - 2]);
cout << g * 2 << endl;
}
int main(){
int n = 4;
print_elements(n);
return 0;
}
실행 결과
6 15 35 14
마무리
이처럼 인접한 소수들의 곱을 활용하면 복잡한 탐색 과정 없이도 주어진 조건을 만족하는 수열을 효율적으로 생성할 수 있습니다. 에라토스테네스의 체로 소수를 미리 구해 두기 때문에 소수 판별 비용이 크게 줄어들고, map을 통한 중복 관리 덕분에 안정적으로 원소를 확장할 수 있어 큰 N에 대해서도 좋은 성능을 보입니다.