📌 강의 바로가기
개념과 코드, 이미지는 해당 책과 강의를 참고하였습니다.
가장 큰 것부터 계획하여 세부적으로 들어가는 방식
작은 것부터 계획하여 하나씩 이어 붙이는 형식
출처: 문제 해결 방식의 두 방향! : 탑다운 과 바텀업 방식
다이나믹 프로그래밍을 이해하고 적용할 수 있는 대표적인 문제로 피보나치 수열
이 있습니다.
피보나치 수열은 다음과 같은 형태의 수열이며, 다이나믹 프로그래밍으로 효과적으로 계산할 수 있습니다.
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...
각각 앞의 항들의 합으로 이어지는 수열을 피보나치 수열이라 합니다.
...
배열
이나 리스트
를 이용해 표현합니다.f(4)를 구하기 위해 호출했을 때 재귀적으로 f(3)와 f(2)를 알고 있어야 하며,
다만, f(3)은 f(2)와 f(1)을 알고 있어야 합니다.
이 각각의 값을 구하기 위해 어떤 값들을 필요로 하는지 확인할 수 있고, 점화식으로 표현할 수 있는 구조는 재귀 함수
를 이용해 작성할 수 있습니다.
# 피보나치 함수(Fibonacci Function)을 재귀함수로 구현
def fibo(x):
if x == 1 or x == 2:
return 1
return fibo(x-1) + fibo(x-2)
print(fibo(4))
실행결과
3
트리 구조를 확인 했을 때 동일한 값이 반복적으로 호출되고 있습니다.
이처럼 f(6)을 호출하면서 불필요한 중복되는 문제가 발생하는 것을 확인했습니다.
이는 수행 시간의 비효율을 불러옵니다.
한 번 해결한 문제를 여러 번 호출되는 것을 막는 방법은 미리 값을 기록해두는 방법이 있는데요.
맨앞에서 언급했듯이 다이나믹 프로그래밍을 구현하는 방법은 두 가지가 있다고 소개 드렸는데요!
첫 번째가 탑다운 방식, 두 번째가 바텀업 방식이었습니다.
이 방식을 이용해봅시다.
메모이제이션
은 탑다운
방식에서 사용되는 방법입니다.
캐싱(Cashing)
이라고도 합니다.# 탑다운 다이나믹 프로그래밍 소스코드
# 한 번 계산된 결과를 메모이제이션(Memoization)하기 위한 리스트 초기화
# 리스트의 크기를 100으로 설정해주기
d = [0] * 100
# 피보나치 함수(Fibonacci Function)를 재귀함수로 구현(탑다운 다이나믹 프로그래밍)
def fib(x):
# 종료 조건 (1 혹은 2일 때 1을 반환)
if x == 1 or x == 2:
return 1
# 이미 계산한 적 있는 문제라면 그대로 반환
if d[x] != 0:
return d[x]
# 아직 계산하지 않은 문제라면 점화식에 따라서 피보나치 결과 반환
d[x] = fib(x-1) + fib(x-2)
return d[x]
print(fib(99))
실행결과
218922995834555169026
# 바텀업 다이나믹 프로그래밍 소스코드
# 앞서 계산된 결과를 저장하기 위한 DP 테이블 초기화
d = [0] * 100
# 첫 번째 피보나치 수와 두 번째 피보나치 수는 1
d[1] = 1
d[2] = 1
n = 99
# 피보나치 함수(Fibonacci Function) 반복문으로 구현(바텀업 다이나믹 프로그래밍)
for i in range(3, n+1):
d[i] = d[i-1] + d[i-2]
print(d[n])
실행결과
218922995834555169026
색칠된 값을 바로 호출하기 때문에 불필요한 연산을 줄여 시간 복잡도를 줄여줍니다.
# 메모이제이션 동작 분석
d = [0] * 100
def fib(x):
print('f(' + str(x) + ')', end=' ')
if x == 1 or x == 2:
return 1
if d[x] != 0:
return d[x]
d[x] = fib(x-1) + fib(x-2)
return d[x]
print(fib(6))
실행결과
f(6) f(5) f(4) f(3) f(2) f(1) f(2) f(3) f(4)
f(6)을 호출하면 f(5) → f(4) → f(3) → f(2) → f(1) → f(2) → f(3) → f(4) 의 형태로 함수가 호출됩니다.
앞에서 배웠던 정렬 파트에서 공부했던 퀵 정렬
처럼 분할 정복과 다이나믹 프로그래밍은 어떤 차이점이 있는지 살펴보겠습니다.
예를 들어, 피벗 값으로 5
를 기준으로 잡으면 배열의 중간으로 들어가며 분할됩니다.
해당 5
의 위치는 그 이후에도 바뀌지 않습니다.
퀵 정렬 동작 원리를 확인해보면, 분할이 이루어진 뒤에
왼쪽에 있는 모든 원소와 오른쪽에 있는 모든 원소에 대해서 다시 한 번 각각 재귀적으로 퀵 정렬을 수행하며,
재귀적으로 호출되는 과정이 모두 종료가 되었을 때, 전체 범위에 대해서 정렬이 수행됩니다.
이처럼 한 번 분할이 이루어지고 나면 더 이상 다른 부분 문제에 포함되지 않고 그 위치가 변경되지 않기 때문에 부분 문제가 중복되지 않는다고 표현할 수 있습니다.