백준 2579번 계단 오르기는 마지막 계단을 반드시 밟되 세 계단을 연속해서 밟을 수 없다는 조건을 점화식으로 옮기는 동적 계획법 문제다. 핵심은 마지막 계단 i에 도착하는 방법을 두 경우로만 나누는 것이다.
dp[i] = score[i] + max(
dp[i - 2],
dp[i - 3] + score[i - 1]
)
원문에는 이 점화식을 찾고도 백트래킹으로 구현하려 했다는 학습 기록이 남아 있었다. 점화식을 세웠다면 “이 값이 어떤 상태를 뜻하는가”와 초기값을 먼저 고정해야 구현이 다른 방향으로 흐르지 않는다.
dp 상태를 한 문장으로 정의한다
dp[i]를 다음처럼 정의한다.
규칙을 지키면서 i번째 계단을 반드시 밟았을 때 얻을 수 있는 최대 점수
마지막 계단을 반드시 밟아야 하므로 답은 dp[n]이다. 단순히 “i번째까지의 최대 점수”라고만 쓰면 i번째 계단을 건너뛴 경우가 섞일 수 있다. 상태 정의에 i번째를 밟는다는 조건을 넣는 것이 중요하다.
마지막 이동은 두 경우뿐이다
i번째 계단에 도착하는 직전 상황을 나눠 보자.
i-2에서 두 칸 올라온 경우
i-1을 밟지 않았으므로 i-2까지의 최적해에 현재 점수를 더할 수 있다.
dp[i - 2] + score[i]
i-1에서 한 칸 올라온 경우
i-1과 i를 연속으로 밟았다. 세 계단 연속을 피하려면 i-2는 밟을 수 없으므로 그 앞은 i-3이어야 한다.
dp[i - 3] + score[i - 1] + score[i]
두 값 가운데 큰 값을 고르면 점화식이 된다.
dp[i] = max(
dp[i - 2] + score[i],
dp[i - 3] + score[i - 1] + score[i]
)
두 식의 공통 항인 score[i]를 밖으로 빼면 처음의 형태와 같다.
초기값은 작은 입력을 직접 적어 정한다
점화식은 i-3을 참조하므로 작은 입력을 별도로 확인한다.
dp[0] = 0
dp[1] = score[1]
dp[2] = score[1] + score[2]
계단이 3개라면 세 개를 모두 밟을 수 없다.
dp[3] = score[3] + max(score[1], score[2])
일반 점화식에 i=3을 넣어도 dp[0]=0 덕분에 같은 결과가 나온다.
dp[3] = score[3] + max(dp[1], dp[0] + score[2])
Java bottom-up 풀이
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
int[] score = new int[n + 1];
for (int i = 1; i <= n; i++) {
score[i] = Integer.parseInt(br.readLine());
}
if (n == 1) {
System.out.println(score[1]);
return;
}
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = score[1];
dp[2] = score[1] + score[2];
for (int i = 3; i <= n; i++) {
dp[i] = score[i] + Math.max(
dp[i - 2],
dp[i - 3] + score[i - 1]
);
}
System.out.println(dp[n]);
}
}
반복문은 각 계단을 한 번씩 처리한다.
- 시간 복잡도:
O(n) - 공간 복잡도:
O(n)
공식 제한에서 계단 수는 최대 300개이고 계단 점수는 최대 10,000이므로 모든 점수를 더한 상한도 3,000,000이다. Java int 범위 안에 들어간다.
왜 그리디로 고르기 어려운가
현재 보이는 두 계단 중 점수가 큰 쪽을 고르는 방식은 이후의 연속 세 계단 제한을 고려하지 못한다. 지금 i-1을 선택하면 다음 선택에서 i-2를 밟았는지까지 영향을 받는다. 지역적으로 큰 점수가 전체 경로의 최대 점수를 보장하지 않는다.
반면 DP는 dp[i-2]와 dp[i-3]에 앞부분의 최적 결과를 저장한다. 마지막 선택만 두 경우로 분리하면 앞의 경로를 다시 전부 탐색할 필요가 없다.
top-down 풀이에서 확인할 부분
원문의 재귀 메모이제이션도 같은 점화식을 사용하므로 방향은 맞다. 다만 다음 항목을 명확히 해야 한다.
n <= 0의 반환값을 0으로 둘 것- 계산하지 않은 상태와 실제 점수 0을 구분할 sentinel을 정할 것
- 재귀 호출 전에
dp[n]을 확인할 것 - 입력 크기가 커질 때 호출 스택 한계를 검토할 것
이 문제의 입력 크기에서는 top-down도 가능하지만, bottom-up은 초기값과 계산 순서가 드러나 디버깅하기 쉽다.
오답을 줄이는 체크리스트
dp[i]가 i번째 계단을 반드시 밟는 상태인지 확인한다.i-1과i를 밟을 때i-2가 제외되는지 본다.n=1,n=2,n=3을 손으로 계산한다.- 마지막 답을 배열 최댓값이 아니라
dp[n]으로 출력한다. - 점화식을 찾은 뒤 다른 탐색 방식으로 구현하고 있지 않은지 점검한다.
참고 자료
'배움과 성장 > 알고리즘·문제풀이' 카테고리의 다른 글
| 백준 2156 포도주 시식 Java: 마지막 잔을 고르지 않는 DP 점화식 (0) | 2022.03.29 |
|---|---|
| 백준 10844 쉬운 계단 수 Java: 자리수 DP와 모듈러 연산 (0) | 2022.03.28 |
| 백준 1932 정수 삼각형 Java: 아래에서 위로 합치는 DP (0) | 2022.03.26 |
| 백준 9251 LCS Java: 두 문자열의 최장 공통 부분 수열 DP (0) | 2022.03.24 |
| 백준 1149 RGB거리 Java: 이전 집의 다른 두 색만 보는 DP (0) | 2022.03.23 |
댓글