씨앗뿌리는 개발자의 개발노트

문제 

N개의 계단이 있고 각 계단 위에는 임의의 정수가 적혀있다. 

철수는 첫 번째 계단에서 마지막 계단까지 올라가려 하는데 한 번에 오를 수 있는 계단수는 1개 또는 2개이다. 

계단을 오르는 비용이 (현재 계단에 적혀있는 정수) - (옮겨갈 계단에 적혀있는 정수)의 절대값이라고 할 때

최소 비용을 구하시오. 

풀이

일단 각 계단에 적힌 정수를 h(i)라고 하자. 

그러면 첫 번째 위치 h(1)에서

한 계단을 오르는 비용은 |h(1) - h(2)| 이고

두 계단을 오르는 비용은 |h(1) - h(3)| 이 된다. 

 

제일 먼저 떠오르는 순진한 방법은 greedy하게 각 계단에서 비용이 적은 층으로 옮겨가는 것이다. 

즉, 단순히 각 계단에서 한 계단 오르는 비용과 두 계단 오르는 비용 중에서 적은 방법을 선택해서 끝까지 가는 것이다. 

 

하지만 greedy를 선택할 때는 항상 소탐대실을 주의해야 한다.

당장의 이익이 커보이지만 전체적으로는 최적이 아닐 수 있기 때문이다. 

greedy는 구현이 쉽지만 이게 정말 최적인지를 고민해 봐야 한다. 

 

여기서는 최적이라고 쉽게 확신할 수 있는 동적계획법으로 풀어보도록 하겠다. 

 

우리가 찾고자 하는 값은 1번째 위치에서 N번째 위치로 가는 최소값이다. 

이를 f(N)이라고 하자. 

 

f(N) = min(

                   f(N-1) + |h(N-1) - h(N)|,

                   f(N-2) + |h(N-2) - h(N)|) 

이 된다. 

해석하자면 N-1번째 계단까지 가는 최소비용에서 1칸을 뛰는 비용과 N-2번째 계단까지 가는 최소비용에서 2칸을 뛰는 비용 중 적은 값이 N까지 가는 최소값인 것이다. 

 

첫 몇 계단에 대한 비용을 생각해보자. 

f(1) = h(1)

f(2) = |h(1) - h(2)|

f(3) = min( 

                f(2) + |h(2) - h(3)|, 

                f(1) + |h(1) - h(3)|)

f(4) = min(

                f(3) + |h(3) - h(4)|, 

                f(2) + |h(2) - h(4)|)

위와 같은 패턴으로 f(1)과 f(2)는 시작값으로 설정하고 이어지는 f(3)부터는 위의 값들을 재활용하여 점점 채워나갈 수 있다. 

 

이는 다음과 같은 수도코드로 작성할 수 있다. 

int calcMininumCostToGetStepN(int N, int[] height) {
    int dp[] = new dp[N];
    
    dp[0] = height[0]
    dp[1] = abs(height[0] - height[1]);
    
    for (int i = 2; i < N; i++) {
        dp[i] = min(dp[i-1] + abs(height[i-1] - height[i]), 
                    dp[i-2] + abs(height[i-2] - height[i])); 
    }
    return dp[N-1];
}

한편 다음과 같이 관계식을 그대로 재귀함수로 옮겨서 구현할 수도 있다. 

f(i) = min(

                 f(i-1) + |h(i-1) - h(i)|,

                 f(i-2) + |h(i-2) - h(i)|)

int dp[] = new dp[N];

int calcRecurMininumCostToGetStepN(int N, int[] height) {
    if (N == 0) {
        dp[0] = height[0];
        return dp[0];
    }
    else if (N == 1) {
        dp[1] = abs(height[0] - height[1]);
        return dp[1];
    }
    
    dp[N-1] = min(calcRecurMininumCostToGetStepN(N-1 + abs(height[i-1] - height[i]), 
                  calcRecurMininumCostToGetStepN(N-2 + abs(height[i-2] - height[i]));   

     return dp[N-1];
}

이때 중요한 것은 메모이제이션을 해서 중복연산을 방지하는 것이다. 

 

회고 

동적계획법 문제를 푸는 첫 번째 접근법은 원하는 답을 f(i)로 정의해 보는 것이다. 

이 문제에서는 N번째 계단까지 가는 최소비용을 f(i)로 정의했다.

그러고 나서 f(i)가 f(i-1), f(i-2)...와 같이 이전값들과 어떤 관계가 있는지 살펴본다. 

그래서 f(i)를 이전 값들을 사용해 관계식을 설정할 수 있으면 동적계획법으로 풀 수 있는 문제에 해당한다. 

profile

씨앗뿌리는 개발자의 개발노트

@씨앗뿌리는 개발자

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!