전체 목록
알고리즘Hard#032

동적 프로그래밍(Dynamic Programming)이란 무엇이며 메모이제이션과 타뷸레이션의 차이는?

#알고리즘#DP#메모이제이션#최적화

답변 포인트

최적 부분 구조와 중복 부분 문제 특성을 생각해보세요.

정답 및 해설

빠른 요약

DP는 복잡한 문제를 부분 문제로 나눠 풀고 결과를 재사용하는 최적화 기법입니다. 적용 조건: 1) 최적 부분 구조 (최적해가 부분 최적해 포함) 2) 중복 부분 문제 메모이제이션 (Top-down): 재귀로 풀면서 결과를 캐싱.

동적 프로그래밍(Dynamic Programming, DP)은 복잡한 문제를 더 작은 부분 문제(Subproblem)로 나누어 풀고, 이미 계산한 결과를 저장하여 재사용함으로써 중복 계산을 제거하는 최적화 기법입니다. 단순 분할 정복(Divide & Conquer)과 달리, DP는 부분 문제들이 서로 겹치는 경우에 효과적입니다. 구현 방식에 따라 하향식(Top-down) 메모이제이션과 상향식(Bottom-up) 타뷸레이션으로 나뉩니다.

DP 적용 조건

동적 프로그래밍을 적용하려면 두 가지 조건을 만족해야 합니다.

1. 최적 부분 구조 (Optimal Substructure)

문제의 최적해가 부분 문제들의 최적해로 구성될 수 있어야 합니다.

알고리즘
  :
A  B  C  D  
A  B   + B  C   + C  D  

fib(n) = fib(n-1) + fib(n-2)
 fib(n)       

2. 중복 부분 문제 (Overlapping Subproblems)

같은 부분 문제가 여러 번 반복해서 계산됩니다.

알고리즘
fib(5)  :
fib(5)
 fib(4)
    fib(3)
       fib(2)   
       fib(1)
    fib(2)       
 fib(3)           
     fib(2)       
     fib(1)

fib(2) 3, fib(3) 2   !

메모이제이션 (Memoization) - Top-down

개념

재귀 + 캐시 방식입니다. 함수를 재귀적으로 호출하되, 이미 계산한 결과는 캐시(메모)에 저장하여 동일한 계산을 반복하지 않습니다.

알고리즘
Top-down:        

피보나치 구현 비교

Python
# 순수 재귀 - O(2ⁿ)
def fib_naive(n):
    if n <= 1:
        return n
    return fib_naive(n-1) + fib_naive(n-2)

# 메모이제이션 적용 - O(n)
def fib_memo(n, memo={}):
    if n in memo:
        return memo[n]  # 이미 계산된 값 반환
    if n <= 1:
        return n

    memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
    return memo[n]

# Python functools.lru_cache 활용
from functools import lru_cache

@lru_cache(maxsize=None)
def fib_cached(n):
    if n <= 1:
        return n
    return fib_cached(n-1) + fib_cached(n-2)

print(fib_cached(100))  # 354224848179261915075 (즉시 계산!)

메모이제이션의 동작 과정

알고리즘
fib_memo(5)  :

 1: fib(5)  memo   fib(4) + fib(3)  
 2: fib(4)  memo   fib(3) + fib(2)  
 3: fib(3)  memo   fib(2) + fib(1)  
 4: fib(2)  memo   fib(1) + fib(0) = 1  memo[2] = 1
 5: fib(1)   1
 3 : fib(3) = 1 + 1 = 2  memo[3] = 2
 6: fib(2)  memo !   1  
 2 : fib(4) = 2 + 1 = 3  memo[4] = 3
 7: fib(3)  memo !   2  
 1 : fib(5) = 3 + 2 = 5  memo[5] = 5

타뷸레이션 (Tabulation) - Bottom-up

개념

반복문 + 테이블 방식입니다. 가장 작은 부분 문제부터 차례로 풀어 올라가며 결과를 테이블(배열)에 저장합니다.

알고리즘
Bottom-up:        

피보나치 타뷸레이션

Python
def fib_tabulation(n):
    if n <= 1:
        return n

    # 테이블 초기화
    dp = [0] * (n + 1)
    dp[0] = 0
    dp[1] = 1

    # 작은 문제부터 순서대로 계산
    for i in range(2, n + 1):
        dp[i] = dp[i-1] + dp[i-2]

    return dp[n]

# dp 테이블 (n=7):
# dp[0]=0, dp[1]=1, dp[2]=1, dp[3]=2, dp[4]=3, dp[5]=5, dp[6]=8, dp[7]=13

# 공간 최적화: O(n) → O(1)
def fib_optimized(n):
    if n <= 1:
        return n
    prev, curr = 0, 1
    for _ in range(2, n + 1):
        prev, curr = curr, prev + curr
    return curr

대표적인 DP 문제

1. 배낭 문제 (0/1 Knapsack)

Python
def knapsack(weights, values, capacity):
    """
    weights: 각 물건의 무게
    values:  각 물건의 가치
    capacity: 배낭의 최대 무게
    """
    n = len(weights)
    # dp[i][w] = i번째 물건까지 고려했을 때 무게 w 이하로 담을 수 있는 최대 가치
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        for w in range(capacity + 1):
            # 현재 물건을 담지 않는 경우
            dp[i][w] = dp[i-1][w]

            # 현재 물건을 담는 경우 (무게가 허용될 때)
            if weights[i-1] <= w:
                dp[i][w] = max(dp[i][w],
                               dp[i-1][w - weights[i-1]] + values[i-1])

    return dp[n][capacity]

weights = [2, 3, 4, 5]
values  = [3, 4, 5, 6]
capacity = 8
print(knapsack(weights, values, capacity))  # 10

2. 최장 공통 부분 수열 (LCS)

Python
def lcs(s1, s2):
    m, n = len(s1), len(s2)
    # dp[i][j] = s1[:i]와 s2[:j]의 LCS 길이
    dp = [[0] * (n + 1) for _ in range(m + 1)]

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i-1] == s2[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1  # 문자 일치
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])  # 최대 선택

    return dp[m][n]

print(lcs("ABCBDAB", "BDCAB"))  # 4 (BCAB 또는 BDAB)

3. 동전 거스름돈 (Coin Change)

Python
def coin_change(coins, amount):
    """최소 동전 개수로 amount 만들기"""
    # dp[i] = 금액 i를 만들기 위한 최소 동전 수
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0  # 금액 0은 동전 0개

    for coin in coins:
        for i in range(coin, amount + 1):
            dp[i] = min(dp[i], dp[i - coin] + 1)

    return dp[amount] if dp[amount] != float('inf') else -1

coins = [1, 5, 10, 25]
print(coin_change(coins, 41))  # 4 (25+10+5+1)

4. 최장 증가 부분 수열 (LIS)

Python
def lis(arr):
    """Longest Increasing Subsequence"""
    n = len(arr)
    # dp[i] = arr[i]로 끝나는 LIS의 길이
    dp = [1] * n

    for i in range(1, n):
        for j in range(i):
            if arr[j] < arr[i]:
                dp[i] = max(dp[i], dp[j] + 1)

    return max(dp)

print(lis([10, 9, 2, 5, 3, 7, 101, 18]))  # 4 (2,3,7,101 또는 2,5,7,101)

메모이제이션 vs 타뷸레이션 상세 비교

Python
# 격자 경로 수 세기 - 두 방식 비교
# m×n 격자에서 왼쪽 위 → 오른쪽 아래로 가는 경우의 수 (오른쪽/아래로만 이동)

# 메모이제이션 방식 (Top-down)
def unique_paths_memo(m, n, memo={}):
    if (m, n) in memo:
        return memo[(m, n)]
    if m == 1 or n == 1:
        return 1

    result = unique_paths_memo(m-1, n, memo) + unique_paths_memo(m, n-1, memo)
    memo[(m, n)] = result
    return result

# 타뷸레이션 방식 (Bottom-up)
def unique_paths_tab(m, n):
    dp = [[1] * n for _ in range(m)]

    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = dp[i-1][j] + dp[i][j-1]

    return dp[m-1][n-1]

print(unique_paths_memo(3, 7))  # 28
print(unique_paths_tab(3, 7))   # 28

DP 접근법 선택 가이드

알고리즘
DP   :
1.    ? YES
2.    ? YES
3.    ? YES
 DP !

 :
-       (  )
-    
-     

 :
-     
-     (   )
-      
-      (n   )

메모이제이션 vs 타뷸레이션 비교표

특성메모이제이션 (Top-down)타뷸레이션 (Bottom-up)
방향큰 문제 → 작은 문제작은 문제 → 큰 문제
구현 방식재귀 + 캐시반복문 + 배열
코드 직관성높음 (점화식과 유사)중간
실행 속도약간 느림 (재귀 오버헤드)빠름
공간 효율필요한 것만 계산모든 상태 계산
스택 오버플로우가능 (재귀 깊이 제한)없음
공간 최적화어려움쉬움 (슬라이딩 윈도우)
적합한 경우일부 상태만 필요할 때모든 상태 계산 시

관련 질문

같은 카테고리/태그 기준