BOJ 5474 Sails

링크

BOJ 5474 Sails

문제 요약

$N$ 개의 돛대가 주어지고, 각 돛대의 높이와 해당 돛대에 달아야 할 돛의 개수가 주어질 때, 저항 값의 합을 최소화하는 문제이다. 이때 저항 값의 합은 각 높이 $i$ 에 $X_i$ 개의 돛이 존재한다고 할 때, $\displaystyle \sum_{i=1}^{100000} { { X_i(X_i -1) } \over {2} }$ 로 정의된다.

관찰

위의 식에서 볼 때 가장 먼저 할 수 있는 생각은 어떤 순서로 돛을 다는지는 중요치 않고, 각 높이에 몇개의 돛을 달았는지가 중요하다는 것이다. 따라서 높이가 가장 낮은 돛 부터 가장 높은 돛 순서대로 문제를 해결하자. 가장 간단히 생각해 볼 수 있는 풀이는 각각의 돛을 달 때마다, 현 시점에서 가장 저항값이 작은 위치에 돛을 다는 그리디를 생각해 볼 수 있다. 또한 같은 저항 값이라면, 최대한 낮은 위치에 다는 것이 이득임을 직관적으로 파악할 수 있을 것이다. 실제로 해당 방법은 정답이 나오고 힙을 이용하여 구현하면, $O(NH\log{H})$ 정도의 시간에 해결이 가능하다.

하지만 입력 범위를 고려하였을 때 위의 풀이는 너무 느리기 때문에, 이 과정을 빠르게 하는 방법을 생각하면 문제가 해결될 것이다. 해당 그리디 알고리즘을 개선하는 방법은 돛을 1개씩 다는 것이 아니라, 한번에 돛대 1개에 달아야 할 돛을 전부 달아보는 것이다. 문제에 주어진 예제를 가지고 이해를 해보도록 하자. 앞서 언급한대로 돛대를 정렬하면 아래와 같다.

2 1
3 2
3 2
4 1
4 3
5 3

이제 각 높이별로 몇 개의 돛이 달려있는지를 나타내면 아래와 같다. 마지막 숫자가 높이 1에 있는 돛의 개수를 의미한다.

돛대 현재 상태
최초 0 0 0 0 0
2 1 0 0 0 0 1
3 2 0 0 1 1 1
3 2 0 0 1 2 2
4 1 0 1 1 2 2
4 3 0 2 2 2 3
5 3 1 2 3 3 3

위의 표에서 진하게 처리된 것은 현재까지 유효한 높이를 의미한다. 표를 보면 알 수 있는 점이 있는데, 바로 값이 더해지는 구간이 많아야 2개의 연속한 구간이란 것이다. 처음 4개의 돛대는 직전의 상태와 비교할 때, 1개의 구간이 업데이트 됨을 알 수 있고, 마지막 2개의 돛대는 2개의 연속한 구간으로 쪼개어져 업데이트 됨을 알 수 있다. 따라서 돛을 1개씩 달지 않고 돛 단위로 처리하는 것이 용이함을 알 수 있다. 이 과정을 처리하는 적절한 자료구조를 이용하면 문제는 풀리게 된다.

풀이

연속 구간에 값을 더하는 자료구조는 대표적으로 세그먼트 트리에 lazy propagation 을 적용시킨 것이 있다. 하지만 본 문제는 이보다 단순한 방법으로 구현이 가능하다. 실제로 우리는 매 순간의 실제 값들이 궁금하지 않고, 나중에 누적된 값을 마지막에 확인하여 저항 값의 합을 구하게 된다. 따라서 이렇게 구간 업데이트를 모두 한 이후에 최종 결과물을 확인하는데 적절한 방법은 imos 법을 이용하는 것이다.

해당 내용에 대하여는 구글링을 하면 잘 나오니 공부해 보도록 하자. 본 내용을 알고 있다면, 위의 표에서 높이 1에 해당되는 값을 왜 마지막에 적었는지도 이해를 할 수 있을 것이다. 왜냐하면 이렇게 할 경우 돛대가 점점 높아지면서, 유효한 높이가 증가하므로 새로운 0 값이 항상 맨 앞에 붙는 꼴이 되기 때문에 imos를 적용하기 편하다. 앞서 언급한 바와 같이 업데이트 할 구간이 1개인지 2개인지는 조금만 고민하면 해결이 되기에 여기서는 생략하고, 대신 본인이 작성한 참고용 코드를 첨부하도록 하겠다.

본 코드는 std::set 을 이용해서 값이 달라지는 위치와 시작점을 관리하였고, 배열 A에 imos를 적용하였다. 업데이트할 구간이 2개인지 여부는 set에서 upper_bound 연산을 통해 위치를 확인하는 과정이 있으므로, 전체 시간 복잡도 $O(N\log{H})$ 에 문제가 해결된다.

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
#include<bits/stdc++.h>
#define F first
#define S second

using namespace std;
typedef long long ll;
typedef pair<int,int> P;

int n;
P m[100005];
int A[100005];
bool in[100005];
set<int> St;

int main()
{
    scanf("%d",&n);
    for(int i=1 ; i<=n ; i++) scanf("%d %d",&m[i].F,&m[i].S);
    sort(m+1,m+1+n); int H=m[n].F;
    St.insert(H+1); in[H+1]=true; A[H+1]=-1;
    for(int i=1 ; i<=n ; i++){
        int cur=H-m[i].F+1;
        if(m[i].F!=m[i-1].F){
            int tmp=*(St.begin());
            if(A[tmp]==0 && in[tmp]) St.erase(tmp), in[tmp]=false;
            St.insert(cur), in[cur]=true;
        }
        auto it=St.upper_bound(cur+m[i].S-1);
        int fn=*it; it--; int st=*it;
        if(fn!=cur+m[i].S){
            int x=(fn-1)-(cur+m[i].S-1-st);
            // x~fn-1, cur~st-1
            A[cur]++, A[st]--;
            A[x]++, A[fn]--;
            if(A[cur]==1 && !in[cur]) St.insert(cur), in[cur]=true;
            if(A[x]==1 && !in[x]) St.insert(x), in[x]=true;
            if(cur!=st && A[st]==0 && in[st]) St.erase(st), in[st]=false;
            if(A[fn]==0 && in[fn]) St.erase(fn), in[fn]=false;
        }
        else{
            //cur~fn-1
            A[cur]++; A[fn]--;
            if(A[cur]==1 && !in[cur]) St.insert(cur), in[cur]=true;
            if(A[fn]==0 && in[fn]) St.erase(fn), in[fn]=false;
        }
    }
    //calculating
    ll ans=0, v=0;
    for(int i=1 ; i<=H ; i++){
        v+=A[i]; ans+=v*(v-1)/2LL;
    }
    printf("%lld",ans);
    return 0;
}