
다이나믹 프로그래밍은 메모리를 적절히 사용하여 수행 시간 효율성을 비약적으로 향상시키는 방법임
이미 계산된 결과(작은 문제)는 별도의 메모리 영역에 저장해 다시 계산하지 않도록 함
다이나믹 프로그래밍의 구현은 일반적으로 두 가지 방식(탑다운:하향식과 보텀업:상향식)으로 구성
동적 계획법이라고도 하며, 프로그램이 실행되는 도중에 실행에 필요하는 메모리를 할당하는 기법이라는 의미
다이나믹 프로그래밍은 문제가 다음의 조건을 만족할 때 사용 가능
1. 최적 부분 구조(Optimal Substructure)
2. 중복되는 부분 문제(Overlapping Subproblem)
피보나치 수열은 다이나믹 프로그래밍으로 효과적으로 계산 가능
피보나치 수열의 형태: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 ...
점화식: 인접한 항들 사이의 관계식
피보나치 수열을 표현한 점화식: an = an-1 + an-2, a1=1, a2=1
재귀함수를 이용해 피보나치 수열을 구현하면 지수 시간 복잡도를 가짐
➡️ 동일한 함수가 여러번 호출되는 비효율이 발생함
메모리제이션이란 다이나믹 프로그래밍을 구현하는 방법 중 하나임
한 번 계산한 결과를 메모리 공간에 메모하는 기법
탑다운(메모리제이션) 방식은 하향식
보텀업 방식은 상향식
다이나믹 프로그래밍의 전형적인 형태는 보텀업 방식
메모리제이션이란 이전에 계산된 결과를 일시적으로 기록해 놓는 넓은 개념을 의미하므로, 다이나믹 프로그래밍에 국한된 개념은 아님
# 메모리제이션을 위한 리스트 초기화
d = [0]*100
def fibo(x):
if x == 1 or x == 2:
return 1
# 이미 계산한 적 있는 문제라면 그대로 반환(계산한 적 있다면, 0이 아닐 것)
if d[x] != 0:
return d[x]
# 아직 계산한 적 없는 문제라면 점화식에 따라 값 저장
d[x] = fibo(x-1) + fibo(x-2)
return d[x]
print(fibo(99))
# 실행 결과
218922995834555169026
d = [0]*100
# 초기 값 지정
d[1] = 1
d[2] = 1
n = 99
# 피보나치 함수를 반복문으로 구현(탑다운 방식의 경우 재귀함수로 구현)
for i in range(3, n+1):
d[i] = d[i-1] + d[i-2]
print(d[n])
# 실행 결과
218922995834555169026
메모리제이션을 활용하는 경우 피보나치 수열 함수 시간 복잡도는 O(N)
(기존 재귀함수 시간 복잡도는 O(2^N)
-> 이미 계산한 결과를 불러오는 것은 N(상수 시간)만 소요되므로, 피보나치 수열을 계산하는데 걸리는 시간복잡도는 O(N)인 것임
다이나믹 프로그래밍과 분할 정복은 모두 최적 부분 구조를 가질 때 사용가능
다이나믹 프로그래밍과 분할 정복의 차이점은 부분 문제의 중복임
주어진 문제가 다이나믹 프로그래밍 유형임을 파악하는 것이 중요
가장 먼저 그리디, 구현, 완전 탐색 등의 아이디어로 문제를 해결할 수 있는지 검토
재귀 함수로 비효율적인 완전 탐색 프로그램을 작성한 뒤에 작은 문제에서 구한 답이 큰 문제에서 그대로 적용 가능하면, 코드를 개선하는 방법을 사용 가능
일반적인 코딩 테스트 수준에서는 기본 유형의 다이나믹 프로그래밍 문제가 출제되는 경우가 많음
인접한 식량 창고는 연속적으로 고를 수 없음, 이때 최대한 많은 식량을 얻는 것이 목표임
ai = i번째 식량창고까지 있을 때, 최적의 해(얻을 수 있는 식량의 최댓값)
a0 = 1
a1 = 3 (창고1)
a2 = 3 (창고1)
a3 = 8 (창고1+창고3)
만약 i번째 식량창고에 대해서 털지 안털지 여부를 결정한다고 하면,
max(i-1까지의 최댓값 vs i-2까지의 최댓값+i번째 식량창고)를 고려하면 됨

이처럼 큰문제를 해결하기 위해 i-1번째 값과 i-2번째 값을 고려하면 됨
ai = i번째 식량창고까지의 최적의 해
ki = i번째 식량창고에 있는 식량의 양
# 식량창고의 개수 입력받기
n = int(input())
array = list(map(int, input().split()))
# dp테이블 초기화
d = [0] * 100
d[0] = array[0]
d[1] = max(array[0],array[1])
# 2부터 n-1까지 반복
for i in range(2, n):
d[i] = max(d[i-1], d[i-2] + array[i])
# 계산 결과 출력
print(d[n-1])
정수 X가 주어졌을 때, 연산 4개를 적절히 사용해서 값을 1로 만들고자 함.
연산을 사용하는 횟수의 최솟값을 출력하시오.
정수가 26이면 다음과 같이 계산해서 3번의 연산이 필요함.
26 -> 25 -> 5 -> 1
그리디 알고리즘이 아닌 DP를 사용해야함 (횟수의 최솟값 문제)
ai = i를 1로 만들기 위한 최소 연산 횟수
점화식: ai = min(ai-1, ai/2, ai/3, ai/5) + 1
# 정수 x를 입력받기
x = int(input())
# 1부터 3만까지 dp 테이블 초기화
d = [0] * 30001
for i in range(2, x+1):
# 현재의 수에서 1을 빼는 경우
d[i] = d[i-1] + 1
# 현재의 수가 2로 나누어 떨어지는 경우
if i % 2 == 0:
d[i] = min(d[i], d[i//2] + 1)
# 현재의 수가 3으로 나누어 떨어지는 경우
if i % 3 == 0:
d[i] = min(d[i], d[i//3] + 1)
# 현재의 수가 5로 나누어 떨어지는 경우
if i % 5 == 0:
d[i] = min(d[i], d[i//5] + 1)
# 네 가지 중에서 가장 작은 수를 출력
print(d[x])
N가지 종류의 화폐가 있음
이 화폐들의 개수를 최소한으로 이용해 그 가치의 합이 M원이 되도록 하려고 함.
각 종류의 화폐는 몇개라도 사용 가능.
M원을 만들기 위한 최소한의 화폐개수를 출력하는 프로그램을 작성하라.
(M원을 만드는 것이 불가능할 경우, -1을 출력하시오)
ai = 금액 i를 만들 수 있는 최소한의 화폐 개수
k = 각 화폐의 단위
점화식: 각 화폐 단위인 k를 하나씩 확인하며,




# 정수 n, m을 입력받기
n, m = map(int, input().split())
# N개의 화폐 단위 정보를 입력받기
array = []
for i in range(n):
array.append(int(input()))
# INF값으로 DP테이블 초기화
d = [10001] * (m+1)
d[0] = 0
for i in range(n): # i는 화폐 단위, j는 금액. 각각의 화폐단위를 loop문 돌리면서 각각의 금액을 한번씩 점검함
for j in range(array[i], m+1):
if d[j - array[i]] != 10001: # i-k원을 만드는 방법이 존재하는 경우
d[j] = min(d[j], d[j-array[i]] + 1) # 현재의 값과 i-k값 사이 최솟값 비교
if d[m] == 10001: # M원을 만드는 방법이 없을 경우
print(-1)
else:
print(d[m])
n * m 크기의 금광이 있음
금광은 1 * 1 크기의 칸으로 나누어져 있으며, 각 칸은 특정 크기의 금이 들어있음
채굴자는 첫번째 열부터 금을 캐기 시작하며, 맨 처음에는 첫번째 열 어느행에서든 출발이 가능
이후 m-1번에 걸쳐서 매번 오른쪽 위, 오른쪽, 오른쪽 아래 3가지 중 하나의 위치로 이동해야함
결과적으로 채굴자가 얻을 수 있는 금의 최대 크기를 출력하시오

특정 위치에서 고려할 경우의 수
1. 왼쪽 위에서 오는 경우
2. 왼쪽 아래에서 오는 경우
3. 왼쪽에서 오는 경우
세가지 중 가장 많은 금을 가지고 있는 경우를 테이블에서 갱신해주기
dp[i][j] = array[i][j] + max(dp[i-1][j-1], dp[i][j-1], dp[i+1][j-1])# test case 입력
for tc in range(int(input())):
# 금광 정보 입력
n, m = map(int, input().split())
array = list(map(int(input().Split()))
# dp테이블 초기화
dp = []
index = 0
# 압력되는 값을 2차원으로 형태 변경해 dp테이블에 넣어줌
for i in range(n):
dp.append(array[index:index + m]
index += m
for j in range(1, m):
for i in range(n):
#왼쪽 위에서 오는 경우
if i == 0:
left_up = 0 # 데이터가 주어진 범위안에 존재하는지 여부
else:
left_up = dp[i-1][j-1]
# 왼쪽 아래에서 오는 경우
if i == n-1:
left_down = 0 # 데이터가 주어진 범위안에 존재하는지 여부
else:
left_down = dp[i+1][j-1]
# 왼쪽에서 오는 경우
left = dp[i][j-1]
dp[i][j] = dp[i][j] + max(left_up, left_down, left)
result = 0
for i in range(n):
result = max(result, dp[i][m-1])
print(result)
N명의 병사가 무작위로 나열되어 있음
각 병사는 특정한 값의 전투력 보유
병사를 배치할 때는 전투력이 높은 병사가 앞쪽에 오도록 내림차순 배치
배치 과정에서 특정한 위치에 있는 병사를 열외시키면서도, 남아있는 병사의 수가 최대가 되도록 함
| 병사 번호 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| 전투력 | 15 | 11 | 4 | 8 | 5 | 2 | 4 |
->
| 병사 번호 | 1 | 2 | 4 | 5 | 7 |
|---|---|---|---|---|---|
| 전투력 | 15 | 11 | 8 | 5 | 4 |
출력시켜야 하는 값은 열외하는 병사의 수
N의 범위가 0 이상 2000 이하이므로, 최대한 O(N^2) 이하의 시간복잡도 필요
이 문제의 기본 아이디어: 가장 긴 증가하는 부분 수열(Longest Increasing Subsequence, LIS)
점화식: 모든 0<=j<i에 대하여, D[i] = max(D[i], D[j]+1) if, array[j] < array[i]

n = int(input())
array = list(map(int, input().split()))
# 순서를 뒤집어 LIS 문제로 변환
array.reverse()
# 1차원 dp 테이블 초기화
dp = [1] * n
# 가장 긴 증가하는 부분 수열(LIS) 알고리즘 수행
for i in range(1,n): # 두번째 원소부터 마지막 원소까지
for j in range(0, i): # i 이전 원소들에 대하여
if array[j] < array[i]: # j 원소의 값이 i 원소값보다 작을 때
dp[i] = max(dp[i], dp[j] + 1)
# 열외해야 하는 병사의 최소 수를 출력
print(n - max(dp))