문제 요약: 산성(양수)과 알칼리성(음수) 특성을 가진 개의 용액이 주어집니다. 이 중 서로 다른 세 개의 용액을 혼합하여, 그 특성값의 합이 0에 가장 가까운 조합을 찾아내야 합니다.
핵심 제약 조건:
용액의 수 : 용액의 수 :
용액의 특성값: (int 범위를 넘을 수 있음)
난이도: Gold 3
문제를 보자마자 바로 코드를 치기보다, 입력 크기를 보고 어떤 알고리즘이 "가능한지" 먼저 판단해야 합니다.
우리가 풀어야 할 문제는 개의 숫자 중 3개를 뽑아 합을 구하는 것입니다. 가장 직관적인 방법은 3중 반복문(또는 재귀를 이용한 조합)을 사용하는 것입니다.
3개의 변수를 동시에 컨트롤하는 것은 복잡합니다. 변수 하나를 고정(Fixing)하여 문제를 단순화시켜 봅시다.
left는 (가장 작은 값 쪽), right는 (가장 큰 값 쪽)에 둡니다.right를 왼쪽으로 이동합니다.left를 오른쪽으로 이동합니다.이 방식을 사용하면, 첫 번째 용액을 고정하는 루프() 투 포인터 탐색() = 시간 복잡도로 해결이 가능합니다.
현재 검사 중인 세 용액의 합을 라고 할 때, 갱신 로직은 다음과 같습니다.
처음에 시도하신 재귀(백트래킹) 방식입니다. 로직 자체는 틀리지 않았으나, 이라는 제약 조건 앞에서 의 벽을 넘지 못했습니다.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
class Main {
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static StringTokenizer st;
static int N;
static long[] A;
static long answer = Long.MAX_VALUE;
static long[] result;
static final int INF = Integer.MAX_VALUE;
public static void main(String[] args) throws IOException {
N = Integer.parseInt(br.readLine());
A = new long[N];
st = new StringTokenizer(br.readLine());
for (int i = 0; i < N; i++) {
A[i] = Long.parseLong(st.nextToken());
}
Arrays.sort(A);
result = new long[3];
// 문제점: N=5000일 때 3중 for문(재귀)은 약 200억 번 이상의 연산을 수행하여 시간 초과 발생
for (int i = 0; i <= N / 2; i++) {
recursion(i, 1, new long[]{A[i], INF, INF});
}
StringBuilder sb = new StringBuilder();
for (long n : result)
sb.append(n).append(' ');
System.out.println(sb);
}
private static void recursion(int start, int count, long[] tmp) {
if (count == 3) {
long sum = tmp[0] + tmp[1] + tmp[2];
if (Math.abs(answer) > Math.abs(sum)) {
answer = sum;
result = tmp.clone();
}
return;
}
for (int i = start + 1; i < N; i++) {
tmp[count] = A[i];
recursion(i, count + 1, tmp);
tmp[count] = INF;
}
}
}
하나를 고정하고 나머지 둘을 투 포인터로 찾는 방식의 코드입니다.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
class Main {
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static StringTokenizer st;
static int N;
static long[] A;
static long answer = Long.MAX_VALUE;
static long[] result;
public static void main(String[] args) throws IOException {
N = Integer.parseInt(br.readLine());
A = new long[N];
st = new StringTokenizer(br.readLine());
for (int i = 0; i < N; i++) {
A[i] = Long.parseLong(st.nextToken());
}
// 투 포인터 활용을 위한 필수 전처리: 정렬
Arrays.sort(A);
result = new long[3];
// 첫 번째 용액을 고정 (i)
for (int i = 0; i < N - 2; i++) {
solv(i);
}
StringBuilder sb = new StringBuilder();
for (long n : result)
sb.append(n).append(' ');
System.out.println(sb);
}
// 고정된 idx에 대해 나머지 두 용액을 투 포인터로 탐색
private static void solv(int idx) {
int left = idx + 1; // 고정된 값 바로 다음부터 시작
int right = N - 1; // 배열의 끝에서 시작
while (left < right) {
long sum = A[idx] + A[left] + A[right];
// 0에 더 가까운 값을 찾으면 갱신
if (Math.abs(answer) > Math.abs(sum)) {
answer = sum;
result[0] = A[idx];
result[1] = A[left];
result[2] = A[right];
}
// 합이 0보다 크면 줄여야 하므로 right 이동
if (sum > 0) {
right -= 1;
}
// 합이 0보다 작으면 키워야 하므로 left 이동
else {
left += 1;
}
}
}
}
이번 문제를 통해 시간 복잡도 분석의 중요성을 다시 한번 체감했습니다.