문제 이해 단계
https://www.acmicpc.net/problem/2473
정수로 되어있는 용액이 N개가 주어진다.
이 중에서 3개를 골라 3개의 합이 0에 가장 가까운 경우를 만들 때,
그때의 세 용액을 찾아서 오름차순으로 출력하는 것이 문제이다.
문제 접근 단계
문제는 상당히 간단한데, 일단 수의 범위랑 제한사항부터 살펴보자.
산성 용액과 알칼리 용액으로 구분해 놨지만,
사실상 용액의 범위는 -1,000,000,000 ~ 1,000,000,000까지이다.
범위가 +-10억을 포함하는 상당히 큰 범위이다.
더해야 하는 작업이 있기 때문에 INT형으로는 처리가 안될 것 같다.
그래서 long long 형으로 받아서 처리하도록 하겠다.
그리고 전체 용액의 개수 (N)은 최대 5,000개까지이다.
이 문제의 시간제한이 1초라는 점을 감안할 때
5,000 * 5,000 = 25 * 10^6 이므로 1초가 넘지 않는다.
따라서 이 문제는 O(n^2)까지는 가능하다.
이 점이 핵심인 것 같아 기억하고 넘어가자.
문제 특성으로 보는 유형 유추
이 문제를 푸는 포인트를, 세 가지 용액이라는 점에서 찾으려고 했다.
세 가지 용액을 합쳐서 0에 최대한 근접해야 한다.
3가지씩 고르는 경우의 수이기 때문에 왠지 브루트포스로 되지 않을까?
그럼 시간복잡도가 O(n^3)이라서 시간초과가 나온다.
여기서 든 생각이, O(n^2)까진 문제에서 허용한다.
그러니까 세 가지 용액 중 하나를 고정시켜 두고,
나머지 2개를 움직이면 O(n^2)니까 충분히 가능하지 않을까?
그래서 특정 합 K을 만들어야 하는데, 용액 2개를 고르는 방법으로 생각했다.
투포인터 방식
2개를 선택한다고 했을 때 떠오른 것은 투포인터 방식이었다.
투포인터는 특정 조건을 만족하는 2개의 값을 찾을 때 많이 쓰니까 생각났다.
그런데 여기서 그냥 투포인터를 사용할 순 없고,
규칙적으로 포인터를 이동하기 위해서는 오름차순 정렬이 필요하다.
오름차순으로 정렬한 다음, 첫 번째 용액 다음 인덱스를 두 번째 용액의 시작점으로 두고,
마지막 인덱스를 세 번째 용액의 시작점으로 두자.
그럼 두 번째 용액은 현재 첫 번째 용액 다음으로 가장 작은 값을 가지고 있고,
마지막 용액은 가장 큰 값을 가지고 있다.
이때 3개의 값을 합해서 0보다 큰 값이 나온다면?
당연히 0과 가까워지기 위해서 값을 줄여야 할 것이다.
그러면 간단하게 마지막 용액의 인덱스를 한 칸 앞으로 당기면 된다.
그렇게 하면 오름차순 정렬했기 때문에 수가 작아지기 때문이다.
반대로 0보다 작은 값이 나온다면 합을 올리기 위해
두 번째 용액의 인덱스를 한 칸 올리면 된다.
이런 식으로 첫 번째 용액이 0번 인덱스 ~ N-2번 인덱스까지 훑을 때,
두 번째와 세 번째 용액이 투포인터로 검사해 주면 된다.
자세한 것은 문제 구현 단계에서 코드로 살펴보도록 하자.
문제 구현 단계
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;
#define INF 999999999999999999 // long long이라 최대한 큰 값
vector<long long> v; // 용액 리스트
int main(){
cin.tie(0); cout.tie(0); ios::sync_with_stdio(false);
int N;
cin >> N;
v.resize(N);
for(int i = 0; i < N; i++) cin >> v[i];
long long cnt = INF; // 현재 0과 가장 가까운 값
long long ans[3]; // 정답 3가지 용액
sort(v.begin(),v.end()); // 오름차순 정렬
// 첫번째 용액 0 ~ N-2까지 탐색
for(int i = 0; i < N-2; i++){
long long fixed = v[i]; // 1번째 용액
long long left = i+1; // 2번째 용액
long long right = v.size()-1; // 3번째 용액
// 두 포인터가 같아질때까지 반복
while(left < right){
long long sum = v[left]+v[right]+fixed; // 합
// 합의 절대값이 현재 cnt보다 작으면 갱신
if(abs(sum) < cnt){
cnt = abs(sum);
ans[0] = fixed;
ans[1] = v[left];
ans[2] = v[right];
}
// 합이 0보다 크면 right 포인터 -1
if(sum > 0) right--;
// 합이 0보다 작으면 left 포인터 +1
else if(sum < 0) left++;
// 만약 합이 0이 나온다면 더이상 할 필요없이 바로 출력해주면됨
else {
cout << fixed << " "<< v[left] << " " << v[right];
return 0;
}
}
}
for(int i = 0; i < 3; i++) cout << ans[i] << " ";
}
풀이는 간단하다.
입력을 받아 오름차순으로 정렬해 주고,
첫 번째 용액을 0 ~ N-2번째 인덱스까지 탐색시키는 동안
투포인터를 사용하여 탐색을 시킨다.
각 경우의 합의 절댓값이 0과 얼마나 가까운지 확인하여
가장 가까운 값을 cnt에 저장한다.
중간에 합이 0이 나온다면,
그것보다 더 나은 답은 나오지 않기 때문에 바로 그 값을 출력해 준다.
여기서 주의해 줄 점은 long long으로 받아주는 것이다.
시행착오
처음에는 2개의 용액을 고정시키고 한 가지를 고르는 방법으로 했는데,
생각해 보니 이 방식은 여전히 O(n^3)이더라.. 그래서 실패했다.
한 가지로 고정하고 투포인터로 하는 게 생각해 보면 당연한데,
떠오르지가 않았다.. 여전히 많이 부족하다. 잘하고 싶다.