중복되는 연산을 줄이자
- 어떤 문제는 메모리 공간을 약간 더 사용하면 연산 속도를 비약적으로 증가시킬 수 있는 방법이 있다.
- 다이나믹 프로그래밍으로 해결할 수 있는 대표적인 예시로는 피보나치 수열이 있다.
- 수학적 점화식을 프로그래밍으로 표현하려면 재귀 함수를 사용하면 간단하다.
# 피보나치 함수를 재귀 함수로 구현
def fibo(x):
if x == 1 or x == 2:
return 1
return fibo(x-1) + fibo(x-2)
print(fibo(4))
- 그런데 피보나치 수열의 소스코드를 이렇게 작성하면 심각한 문제가 생길 수 있다.
- f(n) 함수에서 n이 커지면 커질수록 수행 시간이 기하급수적으로 늘어나기 때문! -> O(2^N)
- 피보나치 수열의 호출 과정을 그림을 그려 확인해보면 동일한 함수가 반복적으로 호출되는 것을 알 수 있다.
- 이미 한 번 계산했지만, 계속 호출할 때마다 계산하는 것이다.
- 이와 같은 문제를 다이나믹 프로그래밍을 사용하면 효율적으로 해결할 수 있다.
조건1) 큰 문제를 작은 문제로 나눌 수 있다.
조건2) 작은 문제에서 구한 정답은 그것을 포함하는 큰 문제에서도 동일하다.
- 메모이제이션 기법(다이나믹 프로그래밍을 구현하는 방법 중 한 종류) = 캐싱
: 한 번 구한 결과를 메모리 공간에 메모해두고 같은 식을 다시 호출하면 메모한 결과를 그대로 가져오는 기법
# 한 번 계산된 결과를 메모이제이션하기 위한 리스트 초기화
d = [0] * 100
# 피보나치 함수를 재귀함수로 구현 (탑다운 다이나믹 프로그래밍)
def fibo(x):
# 종료 조건 (1 혹은 2일 때 1을 반환)
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))
- 정리하자면 다이나믹 프로그래밍이란 큰 문제를 작게 나누고,
같은 문제라면 한 번씩만 풀어 문제를 효율적으로 해결하는 알고리즘 기법이다.
- 분할 정복 알고리즘(퀵정렬)과 다른점은 다이나믹 프로그래밍은 문제들이 서로 영향을 미치고 있다는 것.
- 다이나믹 프로그래밍을 적용했을 때의 피보나치 수열 알고리즘의 시간 복잡도는 O(N)
ㄴ f(1) 구한 값이 f(2) 푸는 데 사용되고,
f(2)의 값이 f(3) 푸는 데 사용되고 ···
ㄴ 한 번 구한 결과는 다시 구해지지 않는다.
# 피보나치에서 호출되는 함수 확인
d = [0] * 100
def pibo(x):
print('f(' + str(x) + ')', end=' ')
if x == 1 or x == 2:
return 1
if d[x] != 0:
return d[x]
d[x] = pibo(x-1) + pibo(x-2)
return d[x]
pibo(6)
- 재귀함수를 이용하여 다이나믹 프로그래밍 소스코드를 작성하는 방법을,
큰 문제를 해결하기 위해 작은 문제를 호출한다고 하여 탑다운 방식이라고 말한다. -> 하향식
- 단순히 반복문을 이용하여 소스코드를 작성하는 경우
작은 문제부터 차근차근 답을 도출한다고 하여 보텀업 방식이라고 말한다. -> 상향식
# 피보나치 수열 (반복적)
# 앞서 계산된 결과를 저장하기 위한 DP 테이블 초기화
d = [0] * 100
# 첫 번쨰 피보나치 수와 두 번째 피보나치 수는 1
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])
실전문제1. 1로 만들기
x = int(input())
# 앞서 계산된 결과를 저장하기 위한 DP 테이블 초기화
d = [0] * 3001
# 다이나믹 프로그래밍 진행 (보텀업)
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])
실전문제2. 개미 전사
n = int(input())
array = list(map(int, input().split()))
# 앞서 계산된 결과를 저장하기 위한 DP 테이블
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])
실전문제3. 바닥 공사
n = int(input())
# 앞서 계산된 결과를 저장하기 위한 DP 테이블 초기화
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])
실전 문제4. 효율적인 화폐 구성
n, m = map(int, input().split())
values = []
for i in range(n):
values.append(int(input()))
# 한 번 계산된 결과를 저장하기 위한 DP 테이블 초기화
dp = [10001] * (m+1)
# 다이나믹 프로그래밍 진행 (보텀업)
dp[0] = 0
for i in range(n):
for j in range(values[i], m+1):
if dp[j - values[i]] != 10001: # (i - k)원을 만드는 방법이 존재하는 경우
dp[j] = min(dp[j], dp[j - values[i]] + 1)
if dp[m] == 10001: # 최종적으로 M원을 만드는 방법이 없는 경우
print(-1)
else:
print(dp[m])
'알고리즘 > 정리' 카테고리의 다른 글
그래프 이론 (Graph) (0) | 2022.07.18 |
---|---|
최단 경로 알고리즘 (Shortest Path Algorithm) (0) | 2022.04.14 |
이진 탐색 알고리즘 (Binary Search Algorithm) (0) | 2022.02.10 |
정렬 알고리즘 (Sorting Algorithm) (0) | 2022.02.04 |
DFS / BFS (깊이우선탐색 / 너비우선탐색) (0) | 2022.01.28 |