자바스크립트로 알고리즘 정리하기 #9 다이나믹 프로그래밍 개념
10을 나눌 때 분할정복이라면 5/5, 6/4, 7/3, 3/7과 같이 중복이 되지 않게 나눈다.10을 나눌 때 다이나믹 프로그래밍이라면 5, 5, 3과 같이 중복이 되게 나눈다.큰 문제 : N번째 피보나치 수를 구하는 문제
작은 문제 : N-1번째 피보나치 수를 구하는 문제, N-2번째 피보나치 수를 구하는 문제
큰 문제2 : N-1번째 피보나치 수를 구하는 문제
작은 문제2 : N-2번째 피보나치 수를 구하는 문제, N-3번째 피보나치 수를 구하는 문제
큰 문제3 : N-2번째 피보나치 수를 구하는 문제
작은 문제3 : N-3번째 피보나치 수를 구하는 문제, N-4번째 피보나치 수를 구하는 문제
주로 이러한 경우에는 재귀를 사용한다.
서울 -> 대전 -> 대구 -> 부산서울 -> 대전 -> 울산 -> 부산이 되는 것이 맞다.let fibo = (n) => (n > 2 ? fibo(n - 2) + fibo(n - 1) : 1);
위는 일반적으로 재귀를 이용하는 피보나치 수 구현이다.
n = 1, 2일 때 초기값 1을 부여해주고 2를 넘는 값이 들어올 때부터는 n-1 n-2 위치에 있는 값을 합한 것이 n이 되는 방식이다.
예로 5번째 피보나치 수를 구할 때 재귀를 이용하면
5번째의 피보나치 수를 구하기 위해
4번째 3번째의 피보나치 수를 구해야 하고
5번째 내부 4번째의 피보나치 수를 구하기 위해
3번째 2번째의 피보나치 수를 구해야 하고
3번째 피보나치 수를 구하기 위해
2번째 1번째의 피보나치 수를 구해야 한다.
5번째 내부 3번째의 피보나치 수를 구하기 위해
3번째 2번째의 피보나치 수를 구해야 하고
3번째 피보나치 수를 구하기 위해
2번째 1번째의 피보나치 수를 구해야 한다.
5번째의 피보나치 수를 구하는데 위와 같은 과정을 거쳐야 한다.
계속 같은 번째 피보나치 수를 구하는 과정을 반복해야 한다.
사실 이미 계산했던 값을 이용하면 되는 것이라 위와 같은 방법으로 구현하면 매우 비효율적이다.
그래서 아래의 방법을 이용할 수 있다.
let fiboArr = [0];
let fiboWithMemoization = (n) => {
if (n < 3) {
fiboArr[n] = 1;
}
if (!fiboArr[n]) { // 내가 저장한 값 중에 없다면 ...
// 재귀를 이용해 구하고 저장
fiboArr[n] = fiboWithMemoization(n - 1) + fiboWithMemoization(n - 2);
}
return fiboArr[n];
};
위는 dp를 이용한 피보나치 수 구현이다. fiboArr이라는 곳에 내가 계산해둔 피보나치 수를 저장해둔다. 이러한 방식의 차이는 속도면에서 어마어마한 차이를 가져온다.
이를테면 50번째 피보나치 수를 구하는 것은 메모이제이션을 사용하지 않은 단순 재귀에서는 어마어마한 시간이 걸리지만, 메모이제이션을 사용한 피보나치 수 구하기 함수를 이용하면 금방 나온다.
n=0일 때1 반환n=1일 때1 반환n=2일 때n=0일 때 계산 1회n=1일 때 계산 1회n=0일 때와 n=1일 때를 더해주는 연산 1회n=3일 때n=2일 때 3회n=1일 때 1회n=2일 때와 n=1일 때를 더해주는 연산 1회n=4일 때n=3일 때 5회n=2일 때 3회n=3일 때와 n=2일 때를 더해주는 연산 1회n=5일 때n=4일 때 9회n=3일 때 5회n=4일 때와 n=3일 때를 더해주는 연산 1회위와 같은 횟수의 연산을 계속한다.
계속 n-1의 연산횟수 + n-2의 연산횟수 + 1회 가 추가된다.

30번째 피보나치 수를 구하는데까지 드는 연산횟수를 프로그램으로 측정해보았다.
총 함수호출이 4356586번만큼 이루어진다.
n=0일 때n[0] = 1n=1일 때n[1] = 1n=2일 때n[2] = n[0] + n[1]n[0]일 때 1회 + n[1]일 때 1회 + 둘을 더하는 1회n=3일 때n[3] = n[1] + n[2]n[1]일 때 1회 + n[2]일 때 1회 + 둘을 더하는 1회위와 같은 숫자로 계산하게 된다. n이 몇이든 계속 3회의 함수호출만 추가된다.

총 함수호출이 86번만큼 이루어진다.
메모이제이션을 하지 않으면, O(대략 피보나치 n번째까지의 총합)만큼의 시간에 값을 구하게 된다. 대략 O(2n제곱) 만큼의 시간복잡도가 걸린다.
메모이제이션을 하면 대략 O(3n)만큼의 시간에 값을 구하게 된다.
앞에 서봤던 재귀 방식은 Top-down 방식이라고 할 수 있음
Bottom-up은 주로 반복문을 사용해서 구현함
// bottom-up의 예제
int d[100];
int fibonacci(int n) {
d[0] = 0;
d[1] = 1;
for(int i=2; i<=n; i++){
d[i] = d[i-1] + d[i-2];
}
return d[n];
}
사실 두 방법 중 하나만 알아도 다이나믹프로그래밍 문제를 푸는데는 큰 지장이 없다.
알 수 없다. 일단은 재귀는 함수 호출 과정이 많이 들어가서 스택 오버플로우를 만들 수도 있고, 반복문이 더 빠를 것 같은데 알 수 없는 이유는 Bottom-up 방식은 정말 모든 문제를 풀고 Top-down은 그렇지 않은 경우가 있기 때문에 알 수 없다.
재귀로 풀었을 때 Stack Overflow가 났다면 다이나믹 프로그래밍을 잘못 구현했을 확률이 높다.
Python 언어를 사용하는 경우에는 스택오버플로 현상 때문에 Top-down보다는 Bottom-up이 유리하다. C, Java 등의 언어에는 상관없다.
대체 가능하다. 대부분 Top-down으로 풀 수 있는 문제는 Bottom-up으로도 풀 수 있다.
조금 어려워지면 점화식의 정의를 조금 변경해야 하는 어려운 문제들이 나온다.
안녕하세요.
JS코드로 구현하면서 dp공부하다가 좋은 정보들 얻어서 감사인사 드립니다.
그런데 하나 여쭤보고 싶은게 있는데요
피보나치 수열 dp 이용한 예시에서
화살표 함수를 사용했을 때와 (let fibo~) 그냥 함수 선언식 사용했을 때
차이가 나는 이유를 혹시 아시나요? 처음에 함수선언식으로 코드를 작성하다 계속 에러가 나서
찾아보다가 이 글을 보고 고치게 됐습니다