메모리를 적절히 사용하여 수행 시간 효율성을 향상 시키는 방법
계산된 결과를 메모리 영역에 저장하여 다시 계산하지 않도록 함
최적 부분 구조와 중복 부분 문제가 만족할 때 사용 가능함
최적 부분 구조 : 큰 문제를 작은 문제로 나눌 수 있으며, 작은 문제의 답을 모아 큰 문제를 해결 할 수 있는 경우
중복 부분 문제 : 동일한 작은 문제를 반복적으로 해결해야 하는 경우
점화식 : 인접한 항들 사이의 관계식
메모이제이션
한 번 계산한 결과를 메모리 공간에 메모하는 기법
같은 문제를 다시 호출하면 메모한 결과를 가져옴
캐시이라고도 함
결과 저장용 리스트를 DP 테이블이라고 부름
탑다운 : 위에서 아래로 내려감, 큰 문제 해결을 위해 작은 문제들을 재귀적으로 호출하여 작은 문제를 먼저 해결
보텀업 : 아래서부터 위로 올라감, 아래쪽부터 작은 계산문제를 하나씩 해결 (반복문 이용)
작은 문제를 먼저 해결해서 점차 올라감
피보나치 수열
재귀함수를 통해 단순히 구현한다면 중복되는 계산이 발생할 수 있음
- 탑다운 방식 피보나치 해결 방법
d = [0] * 100
def fibo(x):
if x == 1 or x == 2:
return 1
if d[x] != 0: # 계산한 적 있는 문제라면
return d[x] # 계산 결과를 가져옴
else: # 계산한 적 없는 문제라면
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 분할 정복
다이나믹 프로그래밍과 분할 정복은 모두 최적 부분 구조를 가질 때 사용이 가능함
분할 정복은 동일한 부분 문제가 반복적으로 계산되지 않음
풀이 알고리즘 선택 방법
주어진 문제가 그리디 , 구현, 완전 탐색으로 해결 가능한지 검토
완전 탐색으로 너무 많은 시간 복잡도가 들게 되면 다이나믹 프로그래밍을 검토
작은 문제를 조합하여 큰 문제를 풀 수 있음, 중복되는 특성을 가짐 -> 다이나믹 프로그래밍
-> 앞부분에서 계산한 결과가 이후 계산할 결과에 영향을 미치는 경우
-> 점화식이 있는 경우 다이나믹 프로그램 -> 점화식을 찾는 것이 중요함!
(다이나믹 프로그래밍 문제는 기본 유형의 문제가 기본적으로 나오기 때문에 반복적인 습득이 필요)
다이나믹 알고리즘 풀이 방법
큰 문제를 통해서 작은 문제를 풀 수 있는가? O
점화식을 도출해야 함!!!!!!!!!!!!!!
DP 테이블 초기화
탑다운/보텀업 중 하나를 선택하여 문제를 해결하면서 작은 결과 값을 DP 테이블에 저장
문제 해결 공식은 도출된 점화식을 그대로 사용
문제) 개미 전사
N개의 창고가 붙어 있을 때 최대한 많은 양의 식량을 털어야함
서로 붙어 있는 창고는 털 수 없음
왼쪽부터 하나씩 계산하면됨
dp[i]를 i 번째까지의 최적의 해라고 가정
data[i] = i번째 창고의 식량 양
i-1번째 창고를 털었다면 i번째 창고를 털 수 없음
-> max(dp[i-1]), dp[i-2] + data[i])
보텀업 방식 사용함
n = int(input()) # 식량 창고의 개수
data = list(map(int, input().split()))
dp = [0] * 100
dp[0] = data[0] # 첫 번째 창고만 터는 경우
dp[1] = max(data[0], data[1]) # 첫 번째와 두 번째 창고를 터는 경우
for i in range(2, n):
dp[i] = max(dp[i-1], dp[i-2] + data[i]) # 점화식 그대로 작성
print(dp[n - 1])
문제) 1로 만들기
정수가 주어졌을 때 (1 <= x <= 30000), 5, 3, 2로 각각 나누어 떨어지면 해당 값으로 나누기
나누어지지 않으면 -1 연산 수행
을 하여 1이 되도록 하는 최소 연산 횟수 구하기
그림을 그려보면 트리구조
작은 계산으로 큰 계산이 가능하므로 다이나믹 알고리즘
dp 테이블 생성
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) # 뒤에 붙은 1은 이번에 계산하는 횟수
if i % 3 == 0:
d[i] = min(d[i], d[i // 3] + 1) # d[i // n]은 이번 계산 후 1이되기 까지 최소 횟수 계산
if i % 5 == 0:
d[i] = min(d[i], d[i // 5] + 1)
print(d[x])
문제) 효율적인 화폐 구성
N가지 종류의 화폐를 사용하여 그 합이 M원이 되도록하는 최소한의 화폐 개수 구하기
출력은 최소 화폐 개수이며, 불가능한 경우 -1 출력
dp[i] = 금액 i를 만들 수 있는 최소 화폐 개수
k = 각 화폐 단위
- 아이디어
각 화폐 단위를 작은 순서대로 따로 따로 확인하자 (이중 for문)
ex) 2, 3원짜리 화폐 단위 확인
무한(한계치를 넘어가는) 값으로 초기화
0원을 0으로 초기화
한 칸씩 올라가면서 이전 값 + 단위 값을 통해 i를 만들 수 있는지 여부를 판단 (i-k를 만드는 방법이 존재하는 경우)
만들 수 있으면 이전 값을 만들 때 사용한 화폐 개수 + 1
-> dp[2]는 dp[0]가 0이기 때문에 0+1
-> 이때 이전 값은 현재 dp[인덱스-현재 화폐 단위]로 확인 가능함
n, m = map(int, input().split())
money = []
for i in range(n):
money.append(int(input()))
dp = [10001] * (m+1) # 10001 == 만들 수 없는 값으로 가정
dp[0] = [0]
for i in range(n): # 화폐 단위만큼 반복
for j in range(money[i], m+1): # 화폐 단위부터 끝까지 반복
if dp[j - money[i]] != 10001: # (현재 탐색 가격 - 화폐 단위)가 계산 가능하다면
dp[j] = min(dp[j], dp[j - money[i]] + 1) # min(기존의 연산, 이전 계산 횟수 + 1)
if dp[m] == 10001: # 계산 방법이 없는 경우
print(-1)
else:
print(dp[m])
※ 해당 포스트는 이것이 "코딩 테스트다 with 파이썬(나동빈 저)"를 통해 학습한 내용을 정리한 것입니다.
[알고리즘] 2차원 배열의 회전 (0) | 2023.01.10 |
---|---|
[코딩테스트] 최단 경로 알고리즘 (0) | 2022.11.23 |
[코딩테스트] 이진 탐색 알고리즘 (0) | 2022.11.21 |
[코딩테스트] 정렬 알고리즘 (0) | 2022.11.20 |
[코딩테스트] DFS/BFS 알고리즘 (0) | 2022.11.19 |