다이나믹 프로그래밍(DP, Dynamic Programming)은 큰 문제를 작은 문제로 나누어 푸는 방법론이다. 이번 글에서는 대표적인 DP 문제 세 가지를 정리하고, 해결 방법을 알아보자.
정수 X가 주어졌을 때, X에 사용할 수 있는 연산은 다음과 같다.
1. X에서 1을 뺀다.
2. X가 2로 나누어떨어지면 2로 나눈다.
3. X가 3으로 나누어떨어지면 3으로 나눈다.
4. X가 5로 나누어떨어지면 5로 나눈다.
이 연산을 이용하여 X를 1로 만들 때 필요한 최소 연산 횟수를 구하라.
각 숫자에 대해 가능한 모든 연산을 고려하여 최적해를 찾는다.
예를 들어, f(6)을 생각해보면 가능한 연산은 다음과 같다.
f(5) (1을 뺀 경우)f(3) (2로 나눈 경우)f(2) (3으로 나눈 경우)따라서 최적해는 다음과 같이 표현할 수 있다.
dp[i] = min(dp[i−1] + 1, dp[i//2] + 1, dp[i//3] + 1, dp[i//5] + 1)
x = int(input())
d = [0] * 30001
for i in range(2, x+1):
d[i] = d[i - 1] + 1
if i % 2 == 0:
d[i] = min(d[i], d[i // 2] + 1)
if i % 3 == 0:
d[i] = min(d[i], d[i // 3] + 1)
if i % 5 == 0:
d[i] = min(d[i], d[i // 5] + 1)
print(d[x])
N개의 식량 창고가 있다. 개미 전사는 인접한 두 개의 식량 창고를 털 수 없다. 최대한 많은 식량을 털었을 때의 값을 구하라.
dp[i] = max(dp[i-2] + a[i], dp[i-1])n = int(input())
a = list(map(int, input().split()))
d = [0] * 100
d[0] = a[0]
d[1] = max(a[0], a[1])
for i in range(2, n):
d[i] = max(d[i-2] + a[i], d[i-1])
print(d[n-1])
가로 길이 N, 세로 길이 2인 바닥을 채우는 방법의 개수를 구하라. 사용할 수 있는 타일은 1×2, 2×1, 2×2 타일이다.
(n-1)까지의 바닥이 채워져 있다면, 1×2 타일을 하나 추가하는 경우 (1가지 방법)(n-2)까지의 바닥이 채워져 있다면, 2×1 두 개 또는 2×2 하나를 추가하는 경우 (2가지 방법) -> 1×2의 경우 1번에서 고려했기 때문에 여기선 제외한다.dp[n] = dp[n-1] + 2 * dp[n-2]n = int(input())
d = [0] * 1001
d[1] = 1
d[2] = 3
for i in range(3, n+1):
d[i] = (d[i-1] + 2 * d[i-2]) % 796796
print(d[n])
N가지 종류의 화폐가 있다. 이 화폐들을 이용하여 합이 M원이 되도록 만들려고 한다. 이때, 사용한 화폐의 개수가 최소가 되도록 하려 한다.
예를 들어, N = 2, M = 15이고, 화폐의 종류가 2원과 3원이라고 가정하자. 이때, 다음과 같이 5개의 화폐를 사용하면 15원을 만들 수 있다.
하지만 2원과 3원을 적절히 사용하면 더 적은 개수의 화폐를 사용할 수도 있다. 최소 개수의 화폐를 사용하는 방법을 찾는 프로그램을 작성하시오.
이 문제는 다이나믹 프로그래밍(DP)을 이용하여 해결할 수 있다. DP 테이블을 활용하여 다음과 같은 점화식을 적용한다.
M+1 크기로 할당하고, 큰 값(예: 10001)으로 초기화한다.dp[0] = 0으로 설정하는데, 이는 아무 화폐도 사용하지 않아도 0원을 만들 수 있기 때문이다.특정 금액 K를 만들기 위해서는, 해당 금액 K에서 한 가지 화폐 단위 C를 뺀 금액 K - C를 만드는 최소 개수에 화폐 하나를 추가하면 된다.
즉, dp[K]는 dp[K - C] + 1이 될 수 있으며, 여러 경우 중 최소 개수를 선택해야 한다.
따라서 점화식은 다음과 같이 표현할 수 있다.
dp[K] = min(dp[K], dp[K - C] + 1)
단, dp[K - C]가 초기값(예: 10001) 그대로라면, K - C원을 만들 방법이 없다는 뜻이다. 즉, 이전 금액을 만들 수 있어야 현재 금액도 만들 수 있기 때문에 이 경우 dp[K]를 갱신하지 않는다.
dp[0]을 0으로 초기화한다.dp[K] 값을 점화식에 따라 갱신해 나간다.초기 DP 테이블 (M = 7)
[0, 10001, 10001, 10001, 10001, 10001, 10001, 10001]
[0, 10001, 1, 10001, 2, 10001, 3, 10001]
[0, 10001, 1, 1, 2, 2, 3, 3]
[0, 10001, 1, 1, 2, 1, 2, 2]
최종적으로 dp[7] = 2가 되어, 최소 2개의 화폐(2원 + 5원)로 7원을 만들 수 있다.
# 입력 받기
n, m = map(int, input().split())
coins = [int(input()) for _ in range(n)]
# DP 테이블 초기화
dp = [10001] * (m + 1)
dp[0] = 0 # 0원을 만들기 위해 필요한 동전 개수는 0개
# 다이나믹 프로그래밍 진행
for coin in coins:
for i in range(coin, m + 1):
if dp[i - coin] != 10001: # 만들 수 있는 경우만 갱신
dp[i] = min(dp[i], dp[i - coin] + 1)
# 결과 출력
print(dp[m] if dp[m] != 10001 else -1)
이 알고리즘은 화폐의 개수 N과 목표 금액 M에 대해 O(NM)의 시간 복잡도를 가진다. N의 최대값이 100이고, M의 최대값이 10,000이므로 최악의 경우 1,000,000번의 연산이 이루어지며, 이는 충분히 해결 가능한 범위이다.
이렇게 다이나믹 프로그래밍을 활용하여 문제를 해결할 수 있다. 핵심은 최적 부분 구조와 중복되는 부분 문제를 인식하고 점화식을 세우는 것이다. 문제를 해결할 때 위 과정을 충분히 연습하면, DP 문제를 쉽게 해결할 수 있을 것이다.