다이나믹 프로그래밍은 메모리를 적절히 사용하여 수행 시간 효율성을 비약적으로 향상시키는 방법입니다.
일반적으로 두가지 방식 탑다운 방식(하양식), 보텀업 방식(상향식) 으로 나뉘어집니다.
동적 계획법 이라고 부르기도 합니다.
완전탐색에서 비효율적인 시간을 가지는 문제라도 다이나믹 프로그래밍을 이용하여 한번 해결한 문제를
다시 해결하지 않도록 별도의 메모리 영역에 저장하여 시간복잡도를 획기적으로 줄일 수 있습니다.
- 최적 부분 구조(Optimal Substructure)
큰 문제를 작은 문제로 나눌 수 있고 작은 문제의 답을 모아 큰 문제를 해결하는 구조- 중복되는 부분 문제(Overlapping Subproblem)
한 번 해결 했던 문제를 다시금 해결한다는 점이 특징
다이나믹 프로그래밍과 분할 정복의 차이점은 부분 문제의 중복입니다.
분할 정복 문제는 동일한 문제가 반복적으로 계산되지 않습니다.
재귀 함수로 비효율적인 완전 탐색 프로그램을 작성한 뒤에 작은 문제에서 구한 답이
큰 문제에서도 그대로 사용되면, 코드를 개선하는 방법을 고려해 볼 수 있습니다.
다이나믹 프로그래밍으로 해결하는 대표적인 예로 피보나치 수열이 있습니다.
피보나치 수열은 첫째 및 둘째 항이 1이며 그 뒤의 모든 항은 바로 앞 두 항의 합인 수열입니다.
# 피보나치 수열
1, 1, 2, 3, 5, 8, 13, 21, 34
점화식으로 표현을 하면 A(n) = A(n-1) + A(n-2) 형태로 반복됩니다.
def fibo(x) :
if x == 1 or x == 2:
return 1
return fibo(x - 1) + fibo(x - 2)
print(fibo(6)) # 실행결과 8
피보나치 수열을 단순 재귀 함수를 사용하면 같은 함수가 여러번 호출 되어
중복되는 부분 문제가 발생하고 시간복잡도는 지수 시간 복잡도를 O(2^n) 가지게 됩니다.
예를들어 30번째 피보나치 수를 구하려고 한다면 약 10억번 가량의 연산을 수행해야 됩니다.
메모이제이션은 한 번 계산한 결과를 메모리 공간에 메모하는 기법입니다.
같은 문제를 다시 호출하면 메모했던 결과를 그대로 가져오고
값을 기록 해놓는다고 해서 캐싱(Cashing) 이라고도 합니다.
결과 저장용 리스트는 DP테이블이라고 부릅니다.
dp = [0] * 100
def fibo(x) :
print('f(' + str(x) + ')', end=' ') # fibo함수가 선언된 순서
if x == 1 or x == 2 : # 첫번째와 두번째 값은 1이라서 조건문에서 분기처리
return 1
if dp[x] != 0: # 이미 계산한 적이 있으면 dp값 리턴
return dp[x]
dp[x] = fibo(x-1) + fibo(x-2)
return dp[x]
fibo(6) # f(6) f(5) f(4) f(3) f(2) f(1) f(2) f(3) f(4) - fibo함수 선언 순서 결과
메모이제이션을 사용하여 피보나치 수열을 구하면 시간 복잡도는 O(N) 입니다.
함수가 호출된 순서를 보시면 탑다운 방식으로 가장 마지막인 f(2)와 f(1)의 값까지 접근하고
해결되지 못한 값에 이미 계산된 값을 리턴하여서 메모리 낭비를 줄이게 됩니다.
dp = [0] * 100
dp[1] = 0
dp[2] = 1
for i in range(3, N + 1) :
dp[i] = dp[i - 1] + dp[i - 2]
print(dp(99)) # 218922995834555169026
보텀업 방식은 반복문을 이용하여 작은 문제를 해결해서 큰 문제를 해결하는 방식입니다.
탑다운 방식과 다른 점은 점화식의 시작항에 대해서 초기화 시켜줍니다.