다이나믹 프로그래밍(Dynamic Programming, 동적 계획법)
- 메모리를 적절히 사용하여 수행 시간 효율성을 비약적으로 향상시키는 방법
- 계산한 결과를 메모리에 저장해(Memoization) 다시 계산하는 일이 없게 함
- 구현 방식은 일반적으로 탑다운방식과 보텀업방식이 있음
- 탑다운방식-큰 문제를 잘게 쪼개어 푸는 것
- 보텀업방식-작은 문제들을 해결해 큰 문제를 해결하는 것
다이나믹 프로그래밍의 조건
주어진 문제가 다음의 조건들을 만족할 때 다이나믹 프로그래밍을 사용할 수 있음
- 최적 부분 구조 (Optimal Substructure)
- 큰 문제를 작은 문제로 나누고 작은 문제의 답을 모아 큰 문제를 해결할 수 있을 때
- 중복되는 부분 문제 (Overlapping Subproblem)
- 동일한 작은 문제를 반복적으로 해결해야 할 때
점화식
- 인접한 항들 사이의 관계식
메모이제이션 (Memoization)
- 다이나믹 프로그래밍을 구현하는 방법 중 하나
- 계산한 결과를 메모리 공간에 메모하는 기법으로 다이나믹 프로그래밍에 국학된 개념은 아님
- 같은 문제를 다시 호출하면 메모했던 결과를 그대로 가져옴
- 값을 기록해둔다는 점에서 캐싱(Caching)이라고도 함
- 계산한 결과를 담아두기만 하고 사용하지 않을 수도 있음
- 결과를 저장하기 위한 리스트를 DP 테이블이라고 부름
탑다운(하향식, 메모이제이션) VS 보텀업(상향식)
- 다이나믹 프로그래밍의 전형적인 형태는 보텀업 방식임
- 탑다운 방식으로 문제를 풀 때 메모이제이션이라는 '하나의 기법'을 사용할 수도 있음
<예제> 피보나치 수열 - 탑다운 방식
d = [0]*100
def fibo(x):
if x==1 or x==2:
return 1
# 이미 계산된 적이 있다면 메모해둔 값을 리턴함
if d[x]!=0:
return d[x]
# 재귀함수 호출로 큰 문제를 작은 문제로 쪼개어 해결해나감
d[x] = fibo(x-1)+fibo(x-2)
return d[x]
print(fibo(99))
<예제> 피보나치 수열 - 보텀업 방식
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])
다이나믹 프로그래밍 VS 분할 정복
- 공통점
- 두 가지 모두 최적 부분 구조를 가질 때 사용할 수 있음
- 최적 부분 구조 - 큰 문제를 작은 문제로 나눌 수 있고, 작은 문제의 답을 모두 큰 문제를 해결할 수 있는 상황
- 두 가지 모두 최적 부분 구조를 가질 때 사용할 수 있음
- 차이점
- 부분 문제의 중복 여부
- 다이나믹 프로그래밍 문제에서는 각 부분 문제들이 서로 영향을 미치며 부분 문제가 중복됨
- 분할 정복 문제에서는 같은 부분 문제가 반복적으로 계산되지 않음
- 부분 문제의 중복 여부
다이나믹 프로그래밍 문제에 접근하는 방법
- 주어진 문제가 다이나믹 프로그래밍 유형인지 파악하는 것이 중요함
- 우선 그리디, 구현, 완전 탐색 등의 아이디어로 해결할 수 있는지 검토함
- 다른 알고리즘 풀이방법이 떠오르지 않으면 다이나믹 프로그래밍을 고려해보도록 함
- 일단 재귀 함수를 이용해 비효율적인 완전 탐색 프로그램을 작성한 뒤에(탑다운) 작은 문제에서 구한 답이 큰 문제에 그대로 사용될 수 있으면 코드를 개선시키는 방법을 사용할 수 있음
- 일반적인 코딩 테스트에서는 기본적인 다이나믹 프로그래밍 문제가 출제되는 편임
<문제> 개미 전사
n = int(input())
array = list(map(int, input().split()))
d = [0]*100
d[0] = array[0]
d[1] = max(array[0], array[1])
for i in range(2,n):
d[i] = max(d[i-1],d[i-2]+array[i])
print(d[n-1])
<문제> 1로 만들기
n = int(input())
d = [0]*27
# 당장 5, 3, 2로 나눠떨어진다고 해도
# 다른 연산을 적절히 섞은 경우가 최소비용일 수 있기 때문에
# 모든 상황을 고려함
for i in range(2,n+1):
d[i] = d[i-1]+1
if i%5==0:
d[i] = min(d[i//5]+1,d[i])
if i%3==0:
d[i] = min(d[i//3]+1,d[i])
if i%2==0:
d[i] = min(d[i//2]+1,d[i])
print(d[n])
<문제> 효율적인 화폐 구성
n,m = map(int,input().split())
# 화폐의 가치를 입력받을 리스트
coins = []
# 입력되는 화폐의 가치가 최대 10000이므로 INF로 초기화
d = [10001]*(m+1)
d[0] = 0
# 화폐의 가치를 입력받음
for i in range(n):
coins.append(int(input()))
# 화폐 종류 개수만큼 반복
for i in range(n):
# 각 화폐의 가치~m까지 반복
for j in range(coins[i], m+1):
# 만약 현재위치-현화폐의값이 10001이 아니라면
# 만들 수 있는 금액이라는 뜻이므로
if d[j-coins[i]]!=10001:
# 현 화폐를 하나 추가하는 경우와
# 기존의 화폐개수 중 작은 것을 비교해 저장함
d[j] = min(d[j-coins[i]]+1, d[j])
# 초기화했던 상태 그대로라면 주어진 화폐로 만들 수 없는 금액이니 -1 출력
if d[n]==10001:
print(-1)
else:
print(d[m])
<문제> 금광
왼쪽대각선 위, 왼쪽, 왼쪽대각선 아래에서 오는 경우 중 최대가 되는 값을 찾아야 함
for tc in range(int(input())):
n,m = map(int(input().split))
mine = list(map(int,input().split()))
dp = []
index = 0
for i in range(n):
dp[n].append(mine[index:index+m])
index += m
for j in range(1,m):
for i in range(n):
# 0번째 행이라면 왼쪽대각선 '위'에서 오는 경우는 없음
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]
# 1번째 열부터 시작하므로 왼쪽에서 오는 경우는 항상 존재함
left = dp[i][j-1]
# 세 방향에서 오는 경우+현재 위치의 값이 최대가 되는 경우를 저장함
dp[i][j] = max(left_up,left_down,left)+dp[i][j]
result = 0
for i in range(n):
result = max(result, dp[i][m-1])
print(result)
<문제> 병사 배치하기
최대 길이 감소 수열을 찾고, 그걸 만들기 위해서 열외시켜야 하는 병사의 출력을 하는 문제다.
얼마 전에 이분 탐색으로 최대 길이 증가 수열을 찾는 방법에 대해 공부했었기 때문에
이분탐색으로도 한 번 풀어봤다.
n = int(input())
# 병사들의 정보를 입력받음
soldiers = list(map(int,input().split()))
# 첫 번째 병사의 정보를 LIS 리스트에 저장함
LIS = [soldiers[0]]
# soldiers 리스트에서 정보를 하나씩 꺼내서 최대 길이 감소 수열을 생성해야 함
for i in range(n):
# LIS에 저장된 마지막 값보다 현재 값이 더 작다면
# LIS에 이어붙임
if LIS[-1]>soldiers[i]:
LIS.append(soldiers[i])
# 그렇지 않다면 LIS에서 이분탐색을 해 soldiers[i]가 들어갈 자리를 찾아야 함
else:
# 시작점과 끝점을 지정함
start, end = 0, len(LIS)
# 탐색이 가능할 때까지
while start<end:
mid = (start+end)//2
# LIS[mid]가 더 크다면 더 오른쪽을 탐색해봐야 함
if LIS[mid]>soldiers[i]:
start = mid+1
# 같거나 작을 때는 왼쪽을 탐색해봐야 함
else:
end = mid
# 제자리에 soldiers[i]를 제자리에 넣음
LIS[end] = soldiers[i]
# 열외할 병사 수 = 병사 수 - 최대길이감소수열의 길이
print(len(soldiers)-len(LIS))
n = int(input())
soldiers = list(map(int,input().split()))
# 각 원소까지의 최대길이감소수열의 길이를 저장할 리스트
dp = [1]*n
# 1번째 병사부터 반복문 시작
for i in range(1,n):
# 자기 앞에 있는 병사들을 탐색함
for j in range(0,i):
# 만약 자기 앞에 전투력이 더 높은 병사가 있다면 == 감소수열을 만족한다면
if soldiers[i]<soldiers[j]:
# 기존에 저장된 감소수열의 길이와, 새로운 감소수열의 길이 중 큰 걸로 갱신
dp[i] = max(dp[i],dp[j]+1)
# 열외할 병사의 수 = 병사의 수 - 최대길이감소수열의 길이
print(n-max(dp))
print(dp)
참고자료 출처
'Algorithm' 카테고리의 다른 글
[최단 경로 알고리즘] Dijkstra, Floyd-Warshall algorithm (1) | 2023.10.06 |
---|---|
[구현] 상하좌우, 시각, 왕실의 나이트, 문자열 재정렬 python (0) | 2023.08.12 |
[그리디 알고리즘] 1이 될 때까지 python (0) | 2023.08.11 |