자바스크립트로 알고리즘 정리하기 #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] = 1
n=1
일 때n[1] = 1
n=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~) 그냥 함수 선언식 사용했을 때
차이가 나는 이유를 혹시 아시나요? 처음에 함수선언식으로 코드를 작성하다 계속 에러가 나서
찾아보다가 이 글을 보고 고치게 됐습니다