Dynamic Programming: 문제를 하위 문제로 나뉘어 푼 다음 그 결과를 바탕으로 문제 해결
(예) 피보나치 수열 솔루션 Fib(n) = Fib(n-1) + Fib(n-2)
def fib(n):
if n == 0 or n == 1:
return 1
return fib(n-1) + fib(n-2)
print(fib(5))
fib(0)
과 fib(1)
의 호출 횟수를 합하면 fib(n)
n = 5
fib = [0 for i in range(n+1)]
fib[0] = 1
fib[1] = 1
for i in range (2, n+1):
fib[i] = fib[i-1] + fib[i-2]
print(fib[5])
n = 5
dp = [-1 for i in range(n+1)]
def fib(x):
if x == 0 or x == 1:
dp[x] = 1 # 초기값 설정, 반환
return dp[x]
if dp[x] != -1:
return dp[x] # 이미 계산된 값이면 바로 반환
dp[x] = fib(x - 1) + fib(x - 2) # 점화식 계산, 저장, 반환
return dp[x]
print(fib(5))
부분문제 설정: 상태와 결과값이 정확히 대응되는 부분 문제 설정
DP(N)
: N칸의 계단을 오르는 경우의 수
점화식 설정: 하위 문제를 합쳐서 상위 문제의 결과를 얻는 방법 도출
DP(N) = DP(N-1) + DP(N-2)
초기값 설정: 가장 기본적인 경우에서의 답을 미리 구해놓음
DP(1) = 1, DP(2) = 2
부분 문제의 답을 확정할 수 있는가?
하위 문제의 답을 구하는데 상위 문제의 답을 활용하지는 않는가?
(예) DP(N): N칸의 계단을 오르는 경우의 수 계단을 내려가는 경우는 없으므로 상위 문제에서 하위 문제로 돌아오지 않는다
점화식에서 누락되거나 중복된 경우가 있는가?
(예) DP(N) = DP(N-1) + DP(N-2)
: 1칸 또는 2칸 둘 중 하나만 선택
초기값을 올바르게 구하였는가?
(예) DP(1) = 1
, DP(2) = 2
> 직접 계산해서 확인
문제: 두 수열이 주어졌을 때, 최장 공통 부분수열을 구한다
(예) ACAYKP
와 CAPCAK
의 최장 공통 부분수열은 ACAK
dp[i][j]
: s의 i번째 문자, t의 j번째 문자까지 봤을 때 LCS의 길이
(예) dp[4][3] = 2
첫번째 경우
점화식 도출
if S(i) == T(j), DP(i, j) = DP(i-1, j-1) + 1
else DP(i, j) = max(DP(i-1, j), DP(i, j-1))
경로 역추적
S(i) == T(j)
일 때 `(i-1, j-1)S(i) != T(j)
일 때, DP(i-1, j)
, DP(i, j-1)
중 큰 것의 인덱스s = input()
t = input()
n = len(s)
m = len(t)
dp.= [[0 for i in range(m+1)] for i in range(n+1)]
prev = [[0, 0] for i in range(m+1)] for i in range(n+1)]
for i in range(1, n+1):
for j in range(1, m+1):
if s[i-1] == t[j-1]:
dp[i][j] = d[i-1][j-1] + 1
prev[i][j] = [i-1, j-1]
else:
if dp[i-1][j] > dp[i][j-1]:
dp[i][j] = dp[i-1][j]
prev[i][j] = [i-1, j]
else:
dp[i][j] = dp[i][j-1]
prev[i][j] = [i, j-1]
LCS 복원하기
lcs = ""
x = n
y = m
while x != 0 and y != 0:
x_next = prev[x][y][0]
y_next = prev[x][y][1]
if x_next + 1 == x and y_next + 1 == y:
lcs = s[x-1] + lcs
x = x_next
y = y_next
print(lcs)
내리막길
Top-down DP
def fib(x):
if x == base_case:
pass # 초기값 저장, 반환
if dp[x] != -1:
pass # 이미 계산된 값 반환
# dp 테이블 채우기
return dp[x]
부분문제 설정: dp[i][j]
: (i, j)에서 출발하는 경로의 개수
점화식 설정: (i, j)에서 출발하는 경로의 개수의 합
초기값 설정: dp[n][m] = 1
- (n, m)은 종착점: (n, m)에서 (n, m)으로 가는 방법은 하나
n, m = map(int, input().split())
a = []
for _ in range(n):
a.append(list(map(int, input().split())))
dp = [[-1 for i in range(m)] for i in range(n)]
dx = [1, 0, -1, 0]
dy = [0, 1, 0, -1]
def f(x, y):
if x == n - 1 and y == m - 1:
dp[x][y] = 1
return 1
if dp[x][y] != -1:
return dp[x][y]
res = 0
for i in range(4):
xx = x + dx[i]
yy = y + dy[i]
if 0 <= xx < n and 0 <= yy < m:
if a[x][y] > a[xx][yy]:
res += f(xx, yy)
dp[x][y] = res
return res
print(f(0, 0))
백준 #12865
N개의 물건이 있고 무게 제한이 있는 가방이 있어 물건 중 선택하여 가장 가치가 크도록 가방을 채우기
DP(i)
: 1~i번 물건까지 선택 여부를 결정했을 때, 배낭 속 물건들의 가치의 최댓값
배낭에 넣을 수 있는 무게 제한 (100,000)
DP(i, j)
: 1~i번 물건까지 선택 여부를 결정했고 총 무게가 j인 경우, 배낭 속 물건들의 가치의 최댓값1~i번 물건 선택 여부 & 무게 j
-> 1~(i-1) 선택, 무게 j
-> 1~(i-1) 선택, 무게 j-wi (wi = i번째 물건의 무게)
DP(i, j) = max(DP(i-1, j), DP(i-1, j-w)+v)(j>=w)
DP(i, j) = DP(i-1, j)(j<w)
i=0
일 때, 아무것도 선택하지 않았으므로 DP(0, 0) = 0
DP(0, j) = -1
i=1
부터 점화식으로 구하자
n, k = map(int, input().split())
dp = [[-1 for i in range(k+1)] for i in range(n+1)]
dp[0][0] = 0
for i in range(1, n+1):
w, v = map(int, input().split())
for j in range(k+1):
if dp[i - 1][j] != -1:
dp[i][j] = dp[i - 1][j]
if j >= w and dp[i - 1][j - w] != -1:
dp[i][j] = max(dp[i][j], dp[i - 1][j - w] + v)
print(max(dp[n])
점화식 다시 만드기
DP(i, j) = max(DP(i-1, j), DP(i-1, j-w)+v) (j >= w)
테이블 두개만 있어도됨
dp: i의 테이블
dp_copy: i가 (i-1)이였을 때의 테이블
n, k = map(int, input().split())
dp = [-1 for i in range(k+1)]
dp[0] = 0
dp_copy = dp.copy()
for i in range(1, n+1):
w, v = map(int, input().split())
for j in range(k+1):
if dp_copy[j] != -1:
dp[j] = dp_copy[j]
if j >= w and dp_copy[j - w] != -1:
dp[j] = max(dp[j], dp_copy[j -w] + v)
dp_copy = dp.copy()
print(max(dp))
백준 #11053
문제: 수열이 주어졌을 때, 순서대로 수를 몇개 뽑았을 때 증가하는 수열 중 가장 긴 것
DP(i)
: A1~Ai로 만들 수 있고 Ai를 반드시 포함하는 LIS
DP(1)부터 DP(i-1)까지 확인한 다음
이 뒤에 Ai를 이어붙이기 (LIS 길이 1 증가)
최댓값이 없을 경우 최댓값은 0으로 간주
시간 복잡도: O(N^2)
n = int(input())
a = list(map(int, input().split()))
dp = [0 for i in range(n)]
dp[0] = 1
for i in range(1, n):
for j in range(i):
if a[i] > a[j]:
dp[i] = max(dp[i], dp[j])
dp[i] += 1
print(max(dp))
같은 길이의 LIS 중 마지막 원소가 가장 작은 것만 남긴다
더 큰 수 뒤에 Ai를 이어붙일 수 있으면 더 작은 수 뒤에도 이어붙일 수 있다
가장 작은 수 뒤에 이어붙일 수 없다면 LIS를 만들 수 없다
길이 i의 LIS 중 가장 작은 마지막 원소를 Bi라 하자
이는 증가수열이다
Bi < B(i-1)이라 가정하자
길이 i인 LIS의 (i-1)번째 원소는 Bi보다 작다
그 원소를 마지막으로 하는 길이 (i-1)의 LIS가 있다
B(i-1)보다 작은 마지막 원소가 있으므로 B의 정의에 모순된다
-> 이분탐색으로 B를 탐색하자
bisect_left
: 정렬된 배열에서 원하는 값 이상의 가장 작은 인덱스 반환bisect_right
: 정렬된 배열에서 원하는 값 미만의 가장 큰 인덱스 반환from bisect import bisect_left
n = int(input())
a = list(map(int, input().split()))
dp = [0 for i in range(n)]
dp[0] = 1
b = [a[0]]
for i in range(1, n):
j = bisect_left(b, a[i])
dp[i] = j + 1
if j == len(b):
b.append(a[i])
else:
b[j] = a[i]
print(max(dp))