[Lv1] 소수 찾기 - 복잡도
입력 n 이하 소수의 개수를 구하는 문제를 단계별로 최적화한 풀이를 정리했습니다. 단순 브루트 포스 방식부터 제곱근까지만 검사하는 방법, 그리고 가장 빠른 에라토스테네스의 체까지 C++ 코드와 채점 결과, 성능 비교를 한눈에 볼 수 있습니다.
해당 문제와 풀이를 보시기 전에 시간 복잡도에 대해 더 자세히 알고 싶으신 분은 아래 글을 참고하시면 됩니다.
문제
문제 설명
1부터 입력받은 숫자 n 사이에 있는 소수의 개수를 반환하는 함수, solution을 만들어 보세요.
소수는 1과 자기 자신으로만 나누어지는 수를 의미합니다.
(1은 소수가 아닙니다.)
제한 조건
- n은 2이상 1000000이하의 자연수입니다.
입출력 예
| n | result |
|---|---|
| 10 | 4 |
| 5 | 3 |
입출력 예 설명
입출력 예 #1
1부터 10 사이의 소수는 [2,3,5,7] 4개가 존재하므로 4를 반환
입출력 예 #2
1부터 5 사이의 소수는 [2,3,5] 3개가 존재하므로 3를 반환
풀이1: 브루트 포스 약수 검사 - O(n^2)
#include <iostream>
bool isPrimeNum(int n)
{
if (n < 2)
{
return false;
}
else if (n == 2)
{
return true;
}
else if (n % 2 == 0)
{
return false;
}
for (auto d = 3; d < n; d += 2)
{
if (n % d == 0)
{
return false;
}
}
return true;
}
int solution(int n)
{
int answer = 0;
for (int i = 2; i <= n; ++i)
{
if (isPrimeNum(i))
{
std::cout << i << std::endl;
++answer;
}
}
return answer;
}
가장 쉽고 직관적으로 구현할 수 있는 코드인데, 위는 당연히 문제 취지 상 시간 초과로 테스트 실패한다.
그렇다면, 여기서 시간 복잡도를 줄여 보아야 하는데 두 가지 방법이 있다.
풀이 2: √n까지만 검사 - O(n^1.5)
위 코드에서 조금 더 최적화 할 수 있는 방법은, 약수 검사를 √n까지만 시도하는 것이다. 한 정수 x가 합성수라면 x=ab인 약수 쌍 (a, b)가 존재하는데, 이 때 두 수 중 하나는 반드시 √x 이하가 된다. 따라서 소수 판정 시 2와 홀수 약수만, (d * d <= n) 범위 까지만 검사하면 충분하다.
#include <iostream>
#include <string>
bool isPrimeNum(int n)
{
if (n < 2)
{
return false;
}
if (n == 2)
{
return true;
}
if (n % 2 == 0)
{
return false;
}
for (int d = 3; d * d <= n; d += 2)
{
if (n % d == 0)
{
return false;
}
}
return true;
}
int solution(int n)
{
int answer = 0;
for (int i = 2; i <= n; ++i)
{
if (isPrimeNum(i))
{
++answer;
}
}
return answer;
}
심화: 에라토스테네스의 체 풀이
다른 사람의 풀이를 보던 중, 재미있는 풀이법을 확인했는데 알고보니 이 방식이 가장 정석의 풀이법이었다. 이 방법이 임의의 자연수 n에 대해 그 이하의 소수를 모두 찾는, 가장 간단하고 빠른방법이라고 한다. 결론부터 말하자면, 해당 방식의 시간 복잡도는 다음과 같다.
알고리즘 아이디어
- 불리언 테이블 준비
크기 n+1짜리 배열을 만들고, 처음엔 전부 “소수(true)”라고 가정한다.
단, 0과 1은 소수가 아니므로 false로 처리한다. - 가장 작은 소수부터 배수 제거
- 2부터 시작해서, 현재 수가 소수라면
- 그 수의 배수들을 모두 지운다(false).
- 배수 제거는 p^2부터 시작하면 된다. (그 이전 배수는 이미 더 작은 소수에서 지워졌기 때문)
- √n까지만 반복
- 소수 후보 p는 √n 까지만 검사한다.
- 그 이후의 합성수는 이미 작은 소수들에 의해 모두 제거된다.
- 남은 수는 전부 소수
테이블에 true로 남은 수들의 개수가 곧 소수의 개수다.
구현 예시
#include <vector>
int solution(int n)
{
if (n < 2)
{
return 0;
}
std::vector<bool> is_prime(n + 1, true);
is_prime[0] = false;
is_prime[1] = false;
for (auto p = 2; p * p <= n; ++p)
{
if (is_prime[p])
{
for (int i = p * p; i <= n; i += p)
{
is_prime[i] = false;
}
}
}
int answer = 0;
for (const auto p : is_prime)
{
if (p)
{
++answer;
}
}
return answer;
}
체점 결과 비교
단순 전수 나눗셈. 작은 케이스는 통과하지만 큰 입력에서 시간 초과.
정확성 테스트 상세
| 테스트 | 결과 | 시간 | 메모리 |
|---|---|---|---|
| 1 | 통과 | 0.01ms | 4.17MB |
| 2 | 통과 | 0.01ms | 4.14MB |
| 3 | 통과 | 0.03ms | 3.67MB |
| 4 | 통과 | 0.09ms | 4.15MB |
| 5 | 통과 | 0.05ms | 4.21MB |
| 6 | 통과 | 6.79ms | 4.15MB |
| 7 | 통과 | 0.46ms | 4.22MB |
| 8 | 통과 | 2.49ms | 4.21MB |
| 9 | 통과 | 6.11ms | 4.16MB |
| 10 | 통과 | 3619.72ms | 4.02MB |
| 11 | 실패 (시간 초과) | — | — |
| 12 | 통과 | 4484.43ms | 4.22MB |
| 정확성 총평: 대형 케이스에서 급격한 지연 | |||
효율성 테스트 상세
| 테스트 | 결과 |
|---|---|
| 1 | 실패 (시간 초과) |
| 2 | 실패 (시간 초과) |
| 3 | 실패 (시간 초과) |
| 4 | 실패 (시간 초과) |
| 효율성 총평: 실사용 불가 | |
합성수의 약수 쌍 성질을 이용해 검사 범위를 √까지 축소. 실전에서 체감 향상 큼.
정확성 테스트 상세
| 테스트 | 결과 | 시간 | 메모리 |
|---|---|---|---|
| 1 | 통과 | 0.01ms | 3.70MB |
| 2 | 통과 | 0.01ms | 3.71MB |
| 3 | 통과 | 0.01ms | 3.64MB |
| 4 | 통과 | 0.02ms | 4.08MB |
| 5 | 통과 | 0.02ms | 4.21MB |
| 6 | 통과 | 0.21ms | 3.63MB |
| 7 | 통과 | 0.04ms | 3.66MB |
| 8 | 통과 | 0.10ms | 4.14MB |
| 9 | 통과 | 0.24ms | 4.19MB |
| 10 | 통과 | 14.04ms | 4.21MB |
| 11 | 통과 | 67.31ms | 4.21MB |
| 12 | 통과 | 16.33ms | 4.18MB |
| 정확성 총평: 전체 구간 안정 통과 | |||
효율성 테스트 상세
| 테스트 | 결과 | 시간 | 메모리 |
|---|---|---|---|
| 1 | 통과 | 77.45ms | 3.77MB |
| 2 | 통과 | 72.84ms | 3.89MB |
| 3 | 통과 | 68.03ms | 3.88MB |
| 4 | 통과 | 66.42ms | 3.89MB |
| 효율성 총평: 대형 입력도 여유 있게 통과 | |||
배수 소거 기반의 정석 풀이. 가장 빠르고, 입력이 커질수록 차이가 벌어짐.
정확성 테스트 상세
| 테스트 | 결과 | 시간 | 메모리 |
|---|---|---|---|
| 1 | 통과 | 0.01ms | 4.21MB |
| 2 | 통과 | 0.01ms | 3.63MB |
| 3 | 통과 | 0.01ms | 4.15MB |
| 4 | 통과 | 0.01ms | 4.02MB |
| 5 | 통과 | 0.02ms | 4.16MB |
| 6 | 통과 | 0.03ms | 4.21MB |
| 7 | 통과 | 0.02ms | 4.16MB |
| 8 | 통과 | 0.04ms | 4.20MB |
| 9 | 통과 | 0.04ms | 3.58MB |
| 10 | 통과 | 1.42ms | 3.65MB |
| 11 | 통과 | 2.97ms | 4.15MB |
| 12 | 통과 | 1.74ms | 4.23MB |
| 정확성 총평: 전 구간 최고 속도 | |||
효율성 테스트 상세
| 테스트 | 결과 | 시간 | 메모리 |
|---|---|---|---|
| 1 | 통과 | 3.09ms | 3.96MB |
| 2 | 통과 | 2.91ms | 3.80MB |
| 3 | 통과 | 3.10ms | 3.91MB |
| 4 | 통과 | 2.96ms | 3.80MB |
| 효율성 총평: 최적 (대형 입력에 특히 강함) | |||
| 방법 | 시간복잡도 | 공간복잡도 | 총평 |
|---|---|---|---|
| 브루트 포스 | O(n^2) | O(1) | 큰 입력에서 시간 초과 |
| √n 검사 | O(n^{1.5}) | O(1) | 현실적으로 충분히 빠름 |
| 에라토스테네스의 체 | O(n \log \log n) | O(n) | 정석, 대형 입력 최적 |
※ 수식 렌더링은 KaTeX/MathJax 환경에서 자동 적용됩니다.