
주어진 숫자 중 서로 다른 3개를 골라 더했을 때, 나올 수 있는 소수의 개수를 구하라.
소수란 무엇인가?
소수를 판단하는 방법
어떤 수 N은 작은 수 * 큰 수 형태로 이루어져 있다.
N의 제곱근 보다 큰 수로 N을 나눈다면 그 몫은 이미 이전에 걸러져 있다.
24 = 2 × 12
24 = 3 × 8
24 = 4 × 6
24 = 6 × 4 ← 이 부분부터는 순서만 바뀐다.
24 = 8 × 3
24 = 12 × 2
24의 제곱근 = 약 4.89
4.89보다 큰 24의 약수는 6
하지만 6은 이미 4 * 6에서 걸러진다.
즉, 24의 제곱근 미만인 수만 확인하면 된다.
11이 소수인지 판별하려면?
11의 제곱근 = 약 3.31
3.31 미만인 수 1, 2
소수는 1과 자기 자신 만을 약수로 갖는 수이므로 1은 제외
2는 11의 약수인가? → ❌
그러므로 11은 소수이다.
Java에서 제곱근을 구하려면 Math.sqrt()를 사용하면 된다.
어떤 수 N이 소수인지 판별하려면?
for(int i = 2; i < Math.sqrt(N); i++) {
if(N % i == 0) {
break;
}
}
이런 식으로 작성하면 된다.
위의 방법을 사용하여 코드를 작성해 본다.
→ 3중 for문을 사용하자.
for문의 시간 복잡도: O(n)
2중 for문의 시간 복잡도: O(n^2)
3중 for문의 시간 복잡도: O(n^3)
문제의 제한사항
배열의 길이 n은 3 ≤ n ≤ 50이다.
최악의 경우 50^3까지 반복할 수 있다.
50^3 < 10^8 이므로, 3중 for문을 사용해도 된다.
class Solution {
int answer = 0;
public int solution(int[] nums) {
comb(nums, 0, 0, 0); // 조합 뽑기 시작
// 최초에는 아직 아무것도 뽑지 않았으므로 0, 0, 0
return answer;
}
// count = 뽑은 횟수
// sum = 뽑은 수의 합
// start = 다음에 뽑을 인덱스
void comb(int[] nums, int count, int sum, int start) {
// 3회 뽑았으면 소수 판별 시작
if(count == 3) {
// 합이 소수이면 개수 카운트
if(isPrime(sum)) {
answer++;
}
return;
}
// 반복문을 통한 재귀호출로 다음 숫자 뽑기
for(int i = start; i < nums.length; i++) {
comb(nums, count + 1, sum + nums[i], i + 1);
}
}
boolean isPrime(int n) {
if(n < 2) {
return false;
}
for(int i = 2; i <= Math.sqrt(n); i++) {
if(n % i == 0) return false;
}
return true;
}
}에라토스테네스의 체
주어진 자연수 N이 소수이기 위한 필요 충분 조건은 N이 N의 제곱근보다 크지 않은 어떤 소수로도 나눠지지 않는다.
어떠한 수1과 어떠한 수2를 나누면 몫이 발생하게 되는데 몫과 나누는 수, 둘 중 하나는 반드시 해당 수의 제곱근 이하이기 때문이다.
→ 즉, 2부터 시작해서, 그 수의 배수를 모두 지워나간다. 그러면 남아 있는 수는 모두 소수가 된다.
예시: 1부터 30까지의 모든 소수 구하기
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
2는 소수니까 제외한 후 2의 배수를 모두 제거한다.
2 3 X 5 X 7 X 9 X 11 X 13 X 15 X 17 X 19 X 21 X 23 X 25 X 27 X 29 X
3은 소수이므로 제외한 후 3의 모든 배수를 제거한다.
2 3 X 5 X 7 X X X 11 X 13 X X X 17 X 19 X X X 23 X 25 X X X 29 X
5도 소수이므로 제외한 후 5의 모든 배수를 제거한다.
2 3 X 5 X 7 X X X 11 X 13 X X X 17 X 19 X X X 23 X 25 X X X 29 X
최종적으로 소수만 남게 되었다.
코드로 구현해보자
import java.util.Arrays;
public class SieveExample {
public static void main(String[] args) {
int n = 30; // 1부터 30까지 소수 찾기
boolean[] isPrime = new boolean[n + 1];
Arrays.fill(isPrime, true); // 일단 전부 소수라고 가정
isPrime[0] = isPrime[1] = false; // 0과 1은 소수가 아님
for (int i = 2; i * i <= n; i++) {
if (isPrime[i]) {
for (int j = i * i; j <= n; j += i) {
isPrime[j] = false; // i의 배수 제거
}
}
}
System.out.println("소수 목록:");
for (int i = 2; i <= n; i++) {
if (isPrime[i]) {
System.out.print(i + " ");
}
}
}
}
Arrays.fill(배열명, 값)
→ 배열 내부의 모든 값을 하나로 초기화 하는 메소드.
Arrays.fill(isPrime, true); → isPrime의 모든 인덱스의 값을 true로 초기화 함.
소수는 true, 소수가 아닌 수는 false → 초기에는 모든 수를 소수로 가정!
for문을 i * i부터 시작하는 이유
i * i 이전의 값은 이미 지워졌기 때문이다.
예시
i = 2일 때:
2의 배수는 4, 6, 8, 10, 12, ...
i = 3일 때:
3의 배수는 6, 9, 12, 15, ...
3 * 3 이전의 수는 3 * 2이다.
하지만 이전에 2의 배수를 지우는 과정에서 2 * 3으로 인해 지워졌다.
4일 경우를 확인해 보면, 4 * 2, 4 * 3은 2와 3의 배수를 지우는 과정인 2 * 4, 3 * 4에서 지워졌다.
5일 경우를 확인해 보면, 5 * 4, 5 * 3, 5 * 2 는 모두 2와 3과 4의 배수를 지우는 과정인 2 * 5, 3 * 5, 4 * 5 에서 지워졌다.
결론
i * i 미만의 값은 모두 이전 과정에서 지워졌기 때문에 반복문의 시작 값을 i * i로 설정한다.
시간복잡도: O(Nlog(log N))
✅위의 에라토스테네스의 체를 가지고 코드를 구현해보자
import java.util.Arrays;
class Solution {
public int solution(int[] nums) {
int answer = 0;
// 0. 오름차순 정렬
Arrays.sort(nums);
// 1. 합의 최댓값 구하기
int n = nums.length;
int maxSum = nums[n - 1] + nums[n - 2] + nums[n - 3];
// 2. 에라토스테네스의 체로 소수 판별 배열 만들기
boolean[] isPrime = new boolean[maxSum + 1];
Arrays.fill(isPrime, true); // 일단 전부 소수라고 가정
isPrime[0] = isPrime[1] = false; // 0과 1은 소수가 아님
for (int i = 2; i * i <= maxSum; i++) {
if (isPrime[i]) {
for (int j = i * i; j <= maxSum; j += i) {
isPrime[j] = false;
}
}
}
// 3. 3개 조합 만들기 + 소수 판별
for (int i = 0; i < n - 2; i++) {
for (int j = i + 1; j < n - 1; j++) {
for (int k = j + 1; k < n; k++) {
int sum = nums[i] + nums[j] + nums[k];
if (isPrime[sum]) {
answer++;
}
}
}
}
return answer;
}
}
기존 코드와 시간 복잡도 비교
기존 코드
에라토스테네스의 체를 사용한 코드
- 3중첩 for문: O(N^3)
- 에라토스테네스의 체 만들기: O(Nlog(log N))
- 소수 판별: O(1)
N이 커지게 되면 에라토스테네스의 체 방식이 시간 복잡도가 효율적이다.
class Solution {
public int solution(int[] nums) {
int answer = 0;
int sum = 0;
for(int i = 0; i < nums.length - 2; i++) {
for(int j = i + 1; j < nums.length - 1; j++) {
for(int k = j + 1; k < nums.length; k++) {
sum = nums[i] + nums[j] + nums[k];
boolean isBreak = false;
// 소수 판단
for(int l = 2; l <= Math.sqrt(sum); l++) {
if(sum % l == 0) {
isBreak = true;
break;
}
}
if(!isBreak) {
answer++;
}
}
}
}
return answer;
}
}