본문 바로가기

문제 노트/백준

다리( BOJ 9006 )

문제 : https://www.acmicpc.net/problem/9006

 

문제 파악하기

왼쪽 집에서 오른쪽 집으로 가는 모든 거리의 합을 최소화하기 위해 다리를 놓아야 할 곳을 찾는 문제입니다. 다리의 길이는 항상 2이기 때문에 다리 길이에 대해서는 신경쓸 필요가 없으며, 어느 위치에 다리를 놓을 것인지만 생각하면 됩니다. 다만, N의 최대 범위가 106이며, 입력되는 집의 위치가 -107 ~ 107이기 때문에 단순히 모든 경우를 탐색하는 방법으로는 절대 해결할 수 없습니다. 대신, 중복되는 계산을 효과적으로 줄여서 정답을 찾을 수 있습니다.

 

문제 해결하기

만약 우리가 h 위치에 다리를 놓았다고 가정하면, 우리가 최솟값을 구해야할 수식 f(h)는 다음과 같습니다. 참고로 수식에서 배열 a과 b는 각각 왼쪽과 오른쪽에 위치한 집의 위치를 의미합니다.

f(h) = M*( |a[1]-h| + ... +|a[N]-h| ) + N*( |b[1]-h| + ... + |b[M]-h| )

 

위의 수식에서 주목할 점은 절댓값 계산입니다. h의 값에 따라 수식은 (a[i]-h) 혹은 (h-a[i])로 바뀝니다. 하지만 같은 수식을 사용하는 값들은 한번에 구할 수 있다는 점에 주목해야 합니다. 만약 N=5이며, a[3]까지 h보다 작거나 같다면 절댓값을 계산하는 수식 g(h)는 다음과 같이 작성할 수 있습니다.

g(h) = (h-a[1]) + (h-a[2]) + (h-a[3]) + (a[4]-h) + (a[5]-h) = h*3 - (a[1]+a[2]+a[3]) + (a[4]+a[5]) - h*2

 

수식이 계산하기 간편하게 바뀐 걸 확인했나요? 절댓값 계산은 h의 크기에 따라 배열의 인덱스를 적절하게 설정하면 누적합으로 구할 수 있습니다. h보다 큰 숫자가 처음으로 나오는 위치를 확인한 다음, h보다 작은 값들의 누적합은 h에서 빼고 나머지 값들은 누적합에서 h를 빼면 됩니다. 이렇게 값을 계산하면 O(logN)만에 모든 수식값을 구할 수 있습니다.

 

그렇다면 정답이 될 수 있는 가능성이 있는 숫자들은 어떤 걸까요? 바로, 왼쪽과 오른쪽에 위치한 집의 위치입니다. 집에 위치가 아닌 곳에 다리를 만들게되면 집 위치에 다리를 만들었을 때의 최솟값보다 항상 결괏값이 커지게 됩니다. 간단하게 생각해보면, 더해야하는 값이 한 곳 늘기 때문이라고 할 수 있습니다. 따라서, 우리는 (N+M)개의 숫자 중에 정답이 있으니 (N+M)개의 숫자들만 비교하면 됩니다.

 

사실 좀 더 생각해보면 꼭 (N+M)개의 숫자를 모두 구할 필요는 없습니다. 위의 f(h)를 그래프로 그려보면 볼록한 함수가 그려집니다. 결국 우리는 (N+M)개의 숫자를 정렬한 다음, 변곡점을 지나는 순간 탐색을 종료하면 됩니다. 자세한 소스코드는 다음과 같습니다.

 

소스코드

1
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <stdio.h>
#include <vector>
#include <algorithm>
#define NMAX 1000010
#define lint long long int
using namespace std;
 
int T;
lint t, ret;
lint inp[2][NMAX], pSum[2][NMAX];
vector< lint > num;
 
int idx;
int cnt[2];
unsigned lint tmp, vmin;
 
int main() {
    scanf("%d"&T);
    while(T--) {
        // init
        num.clear();
        
        // input
        scanf("%d %d"&cnt[0], &cnt[1]);
        for(int inpI=0;inpI<2;inpI++) {
            for(int j=1;j<=cnt[inpI];j++) {
                scanf("%lld"&inp[inpI][j]);
                
                num.push_back(inp[inpI][j]);
            }
        }
        
        // sort
        sort(num.begin(), num.end());
        for(int inpI=0;inpI<2;inpI++) {
            sort(inp[inpI]+1, inp[inpI]+cnt[inpI]+1);
            
            for(int j=1;j<=cnt[inpI];j++) pSum[inpI][j] = pSum[inpI][j-1+ inp[inpI][j];
        }
        
        // search
        vmin = 0xFFFFFFFFFFFFFFFF;
        for(int numI=0;numI<num.size();numI++) {
            
            tmp = 0;
            for(int inpI=0;inpI<2;inpI++) {
                auto tidx = upper_bound(inp[inpI]+1, inp[inpI]+cnt[inpI]+1, num[numI]);
                idx = (int)(tidx - inp[inpI]);
                
                
                // 건너편 집 개수만큼 곱해주기 >> cnt[1-inpI]
                // 현재 다리보다 아래쪽에 있는 모든 집과 다리까지 거리의 합
                tmp += cnt[1-inpI]*( num[numI]*(idx-1- pSum[inpI][idx-1] );
                
                // 현재 다리보다 위쪽에 있는 모든 집과 다리까지 거리의 합
                tmp += cnt[1-inpI]*( (pSum[inpI][cnt[inpI]]-pSum[inpI][idx-1]) - num[numI]*(cnt[inpI]-idx+1) );
            }
            
            // 종료조건
            if(vmin < tmp) break;
            else if(vmin == tmp) continue;
            else {
                vmin = tmp;
                ret = num[numI];
            }
        }
        
        // print
        printf("%lld.0\n", ret);
    }
}
 
cs

후기

자잘한 구현에서 실수를 해서 생각보다 오래 걸렸던 문제입니다. 특히 계산한 값의 범위가 long long int형보다 살짝 커서 unsinged long long int형을 사용해야 한다는 걸 늦게 떠올렸던 점이 발목을 잡았습니다. 어려운 자료구조 없이 최적화를 연습할 수 있어서 재미있던 문제입니다. 

 

'문제 노트 > 백준' 카테고리의 다른 글

Escaping( BOJ 20041 )  (0) 2022.06.20
나무 막대( BOJ 7344 )  (0) 2022.06.19
동전 문제( BOJ 1398 )  (0) 2022.06.11
게리맨더링( BOJ 17471 )  (0) 2022.06.07
창업( BOJ 16890 )  (0) 2022.05.31