DP 패턴 | 동적 프로그래밍 유형별 풀이 전략

DP 패턴 | 동적 프로그래밍 유형별 풀이 전략

이 글의 핵심

DP 패턴에 대한 실전 가이드입니다. 동적 프로그래밍 유형별 풀이 전략 등을 예제와 함께 상세히 설명합니다.

들어가며

”DP 문제는 패턴이다”

대부분의 DP 문제는 몇 가지 패턴의 변형입니다. 패턴을 익히면 새로운 문제도 쉽게 풀 수 있습니다.


1. 1차원 DP 패턴

1차원 DP에서 dp[i]는 보통 “첫 번째부터 i번째까지” 또는 “길이 i인 부분 문제의 최적값”을 뜻합니다. 아래 두 예는 이전 칸의 값만으로 다음 칸을 채우는 대표 패턴입니다.

패턴 1: 이전 값 활용

def climb_stairs(n):
    """
    계단 오르기 (1칸 또는 2칸)
    dp[i] = dp[i-1] + dp[i-2]
    """
    if n <= 2:
        return n
    
    dp = [0] * (n + 1)
    dp[1], dp[2] = 1, 2
    
    for i in range(3, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    
    return dp[n]

print(climb_stairs(5))  # 8

패턴 2: 최대/최소 선택

def rob(houses):
    """
    집 털기 (인접한 집은 털 수 없음)
    dp[i] = max(dp[i-1], dp[i-2] + houses[i])
    """
    if not houses:
        return 0
    if len(houses) == 1:
        return houses[0]
    
    dp = [0] * len(houses)
    dp[0] = houses[0]
    dp[1] = max(houses[0], houses[1])
    
    for i in range(2, len(houses)):
        dp[i] = max(dp[i-1], dp[i-2] + houses[i])
    
    return dp[-1]

# 테스트
print(rob([2, 7, 9, 3, 1]))  # 12 (2+9+1)

2. 2차원 DP 패턴

패턴 1: 격자 경로

def unique_paths(m, n):
    """
    m×n 격자에서 경로의 수
    dp[i][j] = dp[i-1][j] + dp[i][j-1]
    """
    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(3, 7))  # 28

패턴 2: LCS (최장 공통 부분 수열)

def lcs(s1, s2):
    """
    Longest Common Subsequence
    "ABCDGH", "AEDFHR" → "ADH" (길이 3)
    """
    m, n = len(s1), len(s2)
    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("ABCDGH", "AEDFHR"))  # 3

3. 배낭 문제 패턴

0-1 배낭

def knapsack_01(weights, values, capacity):
    """
    각 물건을 0개 또는 1개만
    """
    n = len(weights)
    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_01(weights, values, capacity))  # 10

무한 배낭

def knapsack_unbounded(weights, values, capacity):
    """
    각 물건을 무한대로
    """
    dp = [0] * (capacity + 1)
    
    for w in range(capacity + 1):
        for i in range(len(weights)):
            if weights[i] <= w:
                dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
    
    return dp[capacity]

# 테스트
weights = [1, 3, 4]
values = [15, 50, 60]
capacity = 8
print(knapsack_unbounded(weights, values, capacity))  # 120

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

O(n²) 풀이

def lis_n2(arr):
    """
    Longest Increasing Subsequence
    [10, 9, 2, 5, 3, 7, 101, 18] → 4 ([2,3,7,18])
    """
    n = len(arr)
    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)

# 테스트
arr = [10, 9, 2, 5, 3, 7, 101, 18]
print(lis_n2(arr))  # 4

O(n log n) 풀이

import bisect

def lis_nlogn(arr):
    """
    이진 탐색 활용
    """
    dp = []
    
    for num in arr:
        pos = bisect.bisect_left(dp, num)
        
        if pos == len(dp):
            dp.append(num)
        else:
            dp[pos] = num
    
    return len(dp)

# 테스트
arr = [10, 9, 2, 5, 3, 7, 101, 18]
print(lis_nlogn(arr))  # 4

실전 팁

DP 패턴 인식

# 1. 1D DP
# - 이전 값 활용: dp[i] = f(dp[i-1], dp[i-2])
# - 예: 피보나치, 계단 오르기

# 2. 2D DP
# - 격자: dp[i][j] = f(dp[i-1][j], dp[i][j-1])
# - 문자열: dp[i][j] = f(s1[i], s2[j])
# - 예: LCS, 편집 거리

# 3. 배낭
# - 선택/비선택: dp[i][w] = max(안넣음, 넣음)
# - 예: 0-1 배낭, 동전 문제

정리

핵심 요약

  1. 1D DP: 이전 값 활용, 최대/최소 선택
  2. 2D DP: 격자, LCS, 배낭
  3. LIS: O(n²) 또는 O(n log n)
  4. 패턴 인식: 문제 → 패턴 매칭

추천 문제

백준:

프로그래머스:


관련 글