ALGORITHM NOTE1

BOJ 2096 - 내려가기

내려가도 허락받고 내려가!

#algorithm#boj#gold#dp#sliding-window
아카이브로 돌아가기

문제 링크

문제

N줄에 0 이상 9 이하의 숫자가 세 개씩 적혀 있다. 내려가기 게임을 하고 있는데, 이 게임은 첫 줄에서 시작해서 마지막 줄에서 끝나게 되는 놀이이다.

먼저 처음에 적혀 있는 세 개의 숫자 중에서 하나를 골라서 시작하게 된다. 그리고 다음 줄로 내려가는데, 다음 줄로 내려갈 때에는 다음과 같은 제약 조건이 있다. 바로 아래의 수로 넘어가거나, 아니면 바로 아래의 수와 붙어 있는 수로만 이동할 수 있다는 것이다. 이 제약 조건을 그림으로 나타내어 보면 다음과 같다.

별표는 현재 위치이고, 그 아랫 줄의 파란 동그라미는 원룡이가 다음 줄로 내려갈 수 있는 위치이며, 빨간 가위표는 원룡이가 내려갈 수 없는 위치가 된다. 숫자표가 주어져 있을 때, 얻을 수 있는 최대 점수, 최소 점수를 구하는 프로그램을 작성하시오. 점수는 원룡이가 위치한 곳의 수의 합이다.

입력

첫째 줄에 N(1 ≤ N ≤ 100,000)이 주어진다. 다음 N개의 줄에는 숫자가 세 개씩 주어진다. 숫자는 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 중의 하나가 된다.

출력

첫째 줄에 얻을 수 있는 최대 점수와 최소 점수를 띄어서 출력한다.

풀이

현재 문제를 더 작은 부분 문제로 나눌 수 있으므로 다이나믹 프로그래밍으로 보는 것이 자연스럽다. 필요한 상태를 정하고, 이미 계산한 값을 재사용하는 점화식을 세우면 된다.

코드에서는 배열이나 메모이제이션으로 상태 값을 저장하고, 더 작은 범위나 이전 위치에서 현재 상태로 넘어오는 값을 이용해 갱신한다. 특히 구간 DP, 비트마스크 DP, 1차원/2차원 DP처럼 상태 형태를 먼저 분류해 보면 점화식이 훨씬 선명해진다.

결국 중요한 것은 무엇을 기억해야 중복 계산이 사라지는지 찾는 일이다. 상태 정의가 맞으면 전이 자체는 비교적 자연스럽게 따라오는 경우가 많다.

코드

cpp
#include <iostream>
 
using namespace std;
 
int dp_Max[3];
int dp_Min[3];
int arr[3];
 
void solve() {
    int N = 0;
    int temp0, temp1, temp2;
    cin >> N;
 
    cin >> dp_Max[0] >> dp_Max[1] >> dp_Max[2];
    dp_Min[0] = dp_Max[0]; dp_Min[1] = dp_Max[1]; dp_Min[2] = dp_Max[2];
 
    for (int i = 1; i < N; i++) {
        cin >> arr[0] >> arr[1] >> arr[2];
 
        temp0 = dp_Max[0]; temp1 = dp_Max[1]; temp2 = dp_Max[2];
        dp_Max[0] = max(temp0, temp1) + arr[0];
        dp_Max[1] = max(max(temp0, temp1), temp2) + arr[1];
        dp_Max[2] = max(temp1, temp2) + arr[2];
 
        temp0 = dp_Min[0]; temp1 = dp_Min[1]; temp2 = dp_Min[2];
        dp_Min[0] = min(temp0, temp1) + arr[0];
        dp_Min[1] = min(min(temp0, temp1), temp2) + arr[1];
        dp_Min[2] = min(temp1, temp2) + arr[2];
    }
 
    int max_Num = max(max(dp_Max[0], dp_Max[1]), dp_Max[2]);
    int min_Num = min(min(dp_Min[0], dp_Min[1]), dp_Min[2]);
    cout << max_Num << " " << min_Num << '\n';
}
 
int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);
 
    solve();
 
    return 0;
}

복잡도

  • 시간 복잡도: 각 행의 세 칸 상태를 한 번씩 갱신하므로 O(N)O(N)이다.
  • 공간 복잡도: 이전 행의 최댓값/최솟값만 유지하므로 O(1)O(1)이다.

마무리

DP 문제는 상태를 잘 정의하는 순간 절반은 끝난 셈이다. 어떤 값을 기억해야 같은 계산을 반복하지 않는지만 잡으면 흐름이 보인다.