11-1 동적 계획법이란?
11-2 최장 공통 부분 순서
11-3 배낭 채우기
| 항목 | 분할 정복 | 동적 계획법 |
|---|---|---|
| 문제 분할 | O | O |
| 부분 문제의 중복 | ✕ | O (중복 발생) |
| 해답 저장 | ✕ | O (메모리에 저장) |
| 대표 예시 | 병합 정렬, 이진 탐색 | 피보나치 수열, 최장 공통 부분 수열 |
fib(0) = 0
fib(1) = 1
fib(n) = fib(n-1) + fib(n-2) (n ≥ 2)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
mem = [None] * (n + 1)
def fib_dp_mem(n):
if mem[n] is not None:
return mem[n]
if n < 2:
mem[n] = n
else:
mem[n] = fib_dp_mem(n-1) + fib_dp_mem(n-2)
return mem[n]
mem을 이용해 저장def fib_dp_tab(n):
f = [None] * (n + 1)
f[0], f[1] = 0, 1
for i in range(2, n + 1):
f[i] = f[i-1] + f[i-2]
return f[n]
예외
- 이진 탐색, 병합 정렬 등은 부분 문제의 중복이 없기 때문에 동적 계획법에 적합하지 않음
-> 중복되는 계산이 많고, 한번 구한 값을 다시 활용할 수 있으며, 최적의 해를 작은 문제에서 유도할 수 있는 문제
두 문자열에서 순서를 유지한 채로 공통으로 존재하는 가장 긴 부분 문자열의 길이를 찾는 문제
예시
HELLO WORLDGAME OVERE OR (길이 3)유전자 분석, 파일 차이 비교, 문자열 유사도 판단 등 다양한 분야에서 사용됨
def lcs_recur(X, Y, m, n):
if m == 0 or n == 0:
return 0
if X[m-1] == Y[n-1]:
return 1 + lcs_recur(X, Y, m-1, n-1)
else:
return max(lcs_recur(X, Y, m, n-1), lcs_recur(X, Y, m-1, n))
L[i][j] =
0 if i == 0 or j == 0
L[i-1][j-1] + 1 if X[i-1] == Y[j-1]
max(L[i-1][j], L[i][j-1]) otherwise
def lcs_dp(X, Y):
m, n = len(X), len(Y)
L = [[0] * (n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if X[i-1] == Y[j-1]:
L[i][j] = L[i-1][j-1] + 1
else:
L[i][j] = max(L[i-1][j], L[i][j-1])
return L[m][n]

def lcs_dp_traceback(X, Y, L):
i, j = len(X), len(Y)
lcs = ""
while i > 0 and j > 0:
v = L[i][j]
if v > L[i-1][j] and v > L[i][j-1] and v > L[i-1][j-1]:
i -= 1
j -= 1
lcs = X[i] + lcs
elif v == L[i][j-1] and v > L[i-1][j]:
j -= 1
else:
i -= 1
return lcs
X = "GAME OVER"
Y = "HELLO WORLD"
L = [[0] * (len(Y)+1) for _ in range(len(X)+1)]
lcs_dp(X, Y) # 테이블 생성
lcs_dp_traceback(X, Y, L) # LCS = "EOR"
val = [60, 100, 190, 120, 200, 150]
wt = [ 2, 5, 8, 4, 7, 6]
W = 18 # 배낭의 최대 용량
완전탐색 (모든 조합 확인)
탐욕적 접근 (가치/무게 비율 기준 선택)
A(n, W) =
0 if n == 0 or W == 0
A(n-1, W) if wt[n-1] > W
max(
A(n-1, W), # 현재 물건을 넣지 않는 경우
val[n-1] + A(n-1, W - wt[n-1]) # 현재 물건을 넣는 경우
) otherwise
def knapsack_dc(W, wt, val, n):
if n == 0 or W == 0:
return 0
if wt[n-1] > W:
return knapsack_dc(W, wt, val, n-1)
else:
val_with = val[n-1] + knapsack_dc(W - wt[n-1], wt, val, n-1)
val_without = knapsack_dc(W, wt, val, n-1)
return max(val_with, val_without)
def knapsack_dp(W, wt, val, n):
A = [[0] * (W + 1) for _ in range(n + 1)] # 테이블 초기화
for i in range(1, n + 1):
for w in range(1, W + 1):
if wt[i-1] > w:
A[i][w] = A[i-1][w]
else:
val_with = val[i-1] + A[i-1][w - wt[i-1]]
val_without = A[i-1][w]
A[i][w] = max(val_with, val_without)
return A[n][W]
val = [60, 100, 190, 120, 200, 150]
wt = [2, 5, 8, 4, 7, 6]
W = 18
n = len(val)
print("0-1 배낭 문제 (분할 정복):", knapsack_dc(W, wt, val, n))
print("0-1 배낭 문제 (동적 계획법):", knapsack_dp(W, wt, val, n))
0-1 배낭 문제 (분할 정복): 480
0-1 배낭 문제 (동적 계획법): 480
| 방법 | 시간 복잡도 | 공간 복잡도 | 특징 |
|---|---|---|---|
| 분할 정복 | O(2^n) | O(1) | 중복 호출 많음 |
| 동적 계획법 | O(nW) | O(nW) | 효율적, 실전 사용 가능 |