포스트

PS 일기 2024년 2월 8일 (3일 차) (28220번 블록 쌓기)

PS 일기 2024년 2월 8일 (3일 차)

PS 일기란?

PS 실력 향상을 목적으로 날마다 푼 문제를 회고하는 것입니다. 문제의 간단한 해법과 함께 어떤 방식으로 접근했는지, 그리고 문제의 해법을 찾는 데 결정적이었던 깨달음은 무엇이었는지 등을 기록합니다.

오늘의 문제

28220번 블록 쌓기

알고리즘 분류

  • DP
  • 누적합

문제 설명

블록을 쌓을 수 있는 $N$개의 칸이 있고 각 칸에는 $A_i$개의 블록이 쌓여 있습니다. 각 칸의 각 블록은 인접한 칸으로 옮길 수 있습니다. 아래의 조건을 만족하기 위해 블록을 옮기는 최소 횟수를 구해야합니다.

  • 각 칸에 쌓인 블록의 개수가 $L$개 이상 $R$개 이하입니다.
  • 각 칸의 쌓은 블록의 개수는 단조증가합니다.

문제 풀이

관측 1

빌림의 정도를 정수로 표현 할 수 있습니다.

$i$번째 칸에 블록을 추가하기 위해 $j(1 \leq j < i)$에서도 $j(i < j \leq N)$에서도 블록을 가져올 수 있습니다. 다르게 말하면 너무 자유롭습니다. 그래서 빌림을 정의해 이를 표현할 것입니다. $i$번째 칸의 높이를 $h$로 한다고 했을 때 빌름을 $A_i - h$로 정의 할 수 있습니다. 만약 빌림이 음수라면 빌려오는 것이고 양수라면 빌려주는 것입니다.

아래와 같은 입력이 주어질때 빌림은 다음과 같습니다.

1
2
5 3 5
2 0 9 1 4
$h \backslash\ i$12345
3-1-36-21
4-2-45-30
5-3-54-4-1

각 열에 하나의 행을 택하여 그를 모두 더한 값이 0이라면 성공적으로 완료된 것입니다.

관측 2

점화식은 $D_{i, j, k} = \min_{l = L}^{j} D_{i-1, l, k + A_i - l} + abs(k)$ 입니다.

$D_{i, j, k}$를 i번째까지 계산했을때 $i$번칸의 블럭개수가 $j$이고 $k$개의 블록이 부동일때 최소 움직임이라고 정의

관측 3

상한과 하한을 정할 수 있습니다.

$D_{i, j, k}$를 계산할때 $k$는 $\sum_{l=i+1}^{N} L-A_l \leq k \leq \sum_{l=i+1}^{N} R-A_l$ 입니다. $k$는 $i$번째 칸 이후에 얻을 수 있는 최저 빌림의 합과 최대 빌림의 합의 사이여야하기 때문입니다.

관측 4

점화식을 개선할 수 있습니다.

점화식의 정의를 살짝 바꾸면 더 $O(N^3 (R-L))$에 구할 수 있습니다. $D_{i, j, j}$를 $i$번째까지 계산했을때 $i$번칸의 블럭개수가 $j$이하이고 $k$개의 블록이 부동일때 최소 움직임이라고 정의하면 $D_{i, j, k} = \min(D_{i-1, j, k + A_i - j} + \vert k \vert, D_{i, j - 1, k})$입니다.

구현

의존성을 지키기 위해서 점화식의 순서를 조금 바꿨습니다. 또 슬라이딩 윈도우 등의 방법을 이용하면 더 나은 구현을 할 수 있겠지만 저의 머리가 따라가지 못해 단순하게 구현하였습니다. 제 코드를 보는것 보단 KOI 모범코드를 보는 것을 추천합니다. 이 코드는 제가 봐도 못알아 먹겠네요.

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
#include <bits/stdc++.h>

using namespace std;

using ll = long long;

const int MAXN = 105;
const ll INF = 1e18;

ll N, L, R, arr[MAXN], mn[MAXN], mx[MAXN], dp[MAXN][105][10005];

int main()
{
    ios_base::sync_with_stdio(false); cin.tie(NULL);

    cin >> N >> L >> R;
    for(ll i = 1; i <= N; ++i) {
        cin >> arr[i];
    }
    for(ll i = N; i >= 1; --i) {
        mn[i] = L - arr[i] + mn[i+1];
        mx[i] = R - arr[i] + mx[i+1];
        assert(0 <= mx[i] - mn[i] && mx[i] - mn[i] <= 10000);
    }

    fill((ll*)dp, (ll*)dp + sizeof(dp) / sizeof(ll), INF);
    if(0 < mn[1] || mx[1] < 0) goto NOT_FOUND;

    for(ll i = L; i <= R; ++i) {
        ll v = arr[1] - i;
        if(v < mn[2] || mx[2] < v) continue;
        dp[i-L+1][1][v-mn[2]] = abs(v);
    }

    for(ll i = L; i <= R; ++i) {
        for(ll j = 1; j <= N; ++j) {
            if(i == L && j == 1) continue;
            for(ll k = mn[j+1]; k <= mx[j+1]; ++k) {
                ll& cur = dp[i-L+1][j][k-mn[j+1]];

                ll cand = dp[i-L][j][k-mn[j+1]];
                cur = min(cand, cur);

                ll prv = k - (arr[j] - i);
                if(prv < mn[j] || mx[j] < prv) continue;

                cand = dp[i-L+1][j-1][prv-mn[j]] + abs(k);
                cur = min(cand, cur);
            }
        }
    }

    if(dp[R-L+1][N][0] >= INF) goto NOT_FOUND;
    cout << dp[R-L+1][N][0] << "\n";
    return 0;
NOT_FOUND:
    cout << "-1\n";
    return 0;
}
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.