알고리즘/Dynamic Programming

Memoization, Top-down and Bottom-up

Algorithmus 2022. 3. 3. 21:47

DP의 핵심요소는 메모이제이션(캐싱)이라고 할 수 있다. 이미 계산한 결과를 저장하는 재귀라고도 할 수 있다. 기억 공간을 반복될 계산결과의 저장에 할당함으로써 귀중한 연산시간을 절약하는 것이다. DP를 이용해 해결할 수 있는 대표적이며 가장 간단한 예로 피보나치 수열이 있다. 어떻게 이 문제가 재귀, top-down DP, bottom-up DP, 그리고 그 기억공간을 추가로 절감하는 방식으로 수행되는지를 살펴본다.

재귀

피보나치 수열은 수학적으로 다음 점화식(recurrence relation)으로 정의된다.

$$ f_n = f_{n-1} + f_{n-2} $$

$$ f_0 = 0; \ f_1 = 1 $$

n번째 수를 구하기 위해서는 n-1번째, n-2번째 수를 알아야 하고, 그들을 알기 위해서는 다시 점화식을 풀어야 하므로 결국 base case인 f(0), f(1) 까지 가야 점화식의 전개가 끝나고 답을 구할 수 있다. 점화식을 컴퓨터 프로그래밍에서 구현한 것이 재귀(recursion)이라고 할 수 있다. 다음 예를 보자.

 

def fib(n):
    if n == 0 or n == 1: f = n
    return fib(n - 1) + fib(n - 2)

 

이 식의 시간복잡도는 O(2^n) 이 된다.

 

Top-down DP

코드를 먼저 보자.

 

memo = {}
def fib(n):
    if n in memo: return memo[n]
    if n == 0 or n == 1: f = n
    else: f = fib(n - 1) + fib(n - 2)
    memo[n] = f
    return f
# [Source] MIT 6.006, Lec 19 (2011)

 

여기서는 위 코드와 다른 부분으로서 캐시를 활용하는 부분이 추가되었다. 즉, 캐시(memo)안에 n에 대한 피보나치 수가 저장되어 있다면 그것을 돌려준다. 만일 그렇지 않다면, n in (0, 1)인 경우에 f에 n을 저장하거나 그 밖의 경우에는 f에 재귀식을 사용해 값을 계산해 저장한다(만일 재귀식을 호출했는데 그 값은 memo 안에 저장된 것이 있다면 그것을 활용해서 계산해 돌려줄 것이다). f를 새로 계산했으므로 이것은 나중에 활용하기 위해 memo 안에 저장한다. 마지막으로 계산한 f값은 돌려준다.

memo를 직접 하지 않고 @cache나 @lru_cache를 활용하는 방법은 https://algoship.tistory.com/8 에서 소개했으므로 이를 참고하라.

 

Bottom-up DP

상향식 DP는 재귀와 캐시를 사용하지 않고 바로 테이블에 저장한 뒤 마지막 값을 돌려준다.

 

def fib(n):
    table = [0 for _ in range(n+1)]   # 0, 1, 2, ..., n
    for i in range(n+1):
        if i in (0, 1):
            table[i] = i
        else:
            table[i] = table[i - 1] + table[i - 2]
    return table[n]

 

이 경우 바로 array에 값들을 저장하고 불러오는데 이것은 재귀호출과 캐싱을 한 것과 효과가 같다. Bottom-up DP는 재귀를 위한 스택을 준비하지 않아도 되므로 때때로 Top-down DP보다 속도가 더 빠른 장점이 있으나, 만일 Top-down 이었을 경우에 base case까지 도달하기 위한 총 스택의 수를 미리 계산하여 전체 array의 기억공간을 배정해줘야 하는 점이 까다로운 부분이다 (단순히 memo = {} 로 설정했던 Top-down과 비교해보라).

그러나, 이 경우 기억공간이 O(n) 까지 필요가 없을 수 있다. 왜냐하면 현재의 피보나치 수를 위해선 직전과 직직전의 값만이 필요하기 때문이다. 그러므로 아래와 같이 바꾸어 O(1)으로 풀 수 있다. 이러한 공간 절약은 재귀시와는 달리 bottom-up에서 가능하다.

 

def fib(n):
    a, b = 0, 1
    for i in range(n):
        a, b = b, a + b
    return a

# [Source] https://stackoverflow.com/questions/36948082/bottom-up-fibonacci-in-python-using-o1-space

 

Bottom-up은 Topological Sort이라고 할 수 있다. f(n-2) -> f(n-1) -> f(n) 이면서 f(n-2) -> f(n) 이기 때문이다. 즉, f(n)이 그 전단계에 영향을 미치지 않는다. 그러므로 DAG (directed acyclic graph) 와 같은 형태이다.

반응형

'알고리즘 > Dynamic Programming' 카테고리의 다른 글

Removing Digits  (0) 2022.05.04
Coin Combinations II  (0) 2022.05.03
Coin Combinations I  (0) 2022.05.01
DP 실수 모음  (0) 2022.04.29
Greedy vs DP  (1) 2022.04.09