1. 일반적인 표현방법
int func(int n) { if (n == 0) return 0; if (n == 1 || n == 2) return 1; return func(n - 1) + func(n - 2); }
장점: 코드가 직관적이고 이해가 쉬움
단점: 똑같은 연산을 여러번 함으로써 굉장히 비효율적이고 함수가 호출될 때 또다른 함수2개를 호출하므로 연산시간이 길어짐
깊이가 2의 제곱꼴로 깊어짐(1->2->4->8->16 . . )
시간복잡도: 연산의 횟수가 2의 거듭제곱꼴로 지속되므로 O(2^N)
2. 최적화 기법 1 : 동적프로그래밍
핵심 : 딱 한번만 계산하고 저장된 값(배열에 담음)을 호출만 한다.(메모이제이션 기법)
(위의 그림에서 f(n-5)는 7번호출되므로 메모이제이션기법으로 한번만 계산함)
시간복잡도 : 기존에 함수호출의 깊이가 깊어질수록 2의 N승씩 연산했는데, 메모이제이션으로 단 한번씩만 계산하므로 O(N)
코드
#include<iostream> using namespace std; long long dp[50]; long long func(long long x); int main() { cout << "50번째 피보나치 수열은" << endl; cout << func(50) << "입니다." << endl; } long long func(long long x) { if (x <= 2)return 1; //저장된 값이라면 바로 값을 리턴! if (dp[x] != 0) return dp[x]; dp[x] = func(x - 1) + func(x - 2); return dp[x]; }
결과
3. 최적화 기법 2 : 행렬의 성질 이용(분할 정복)
아이디어: 2*2크기의 행렬에 피보나치수열을 표현하여 행렬곱연산(거듭제곱 연산)으로 표현하고싶다.
핵심: 분할정복 기법으로, 일단 잘게 나누고, 나눠진 값들을 합쳐서 최종값을 구함
n번째 피보나치 수열 Fn을 행렬로표현하기
10번째 피보나치 구하기
1단계-> 10번째 피보나치 분할
2단계-> 5번째 피보나치 분할(홀수이므로 제곱연산때처럼 4번째 피보나치*첫번째 피보나치로 나눔)
3단계-> 4번째 피보나치 분할
3단계(마지막)-> 2번째 피보나치 분할
모두 나눴으니 이제 거꾸로 계산하며 합치면 됨(분할->정복)
결과
시간복잡도: n번째 피보나치가 n/2번째피보나치의 제곱->n/4번째 피보나치의 제곱 ...순으로 나눠지므로 최초의 행렬을 logN회 제곱하면 되므로 O(logN)
코드
#include<iostream> using namespace std; struct M { long long data[2][2]; }; long long func(int x); M divide(M a, int n); M multiply(M a, M b); int main() { cout << "46번째 피보나치 수는" << endl; cout << func(46) << "입니다." << endl; } long long func(int x) { M a; //F(1)설정 a.data[0][0] = 1, a.data[0][1] = 1; a.data[1][0] = 1, a.data[1][1] = 0; //분할 a = divide(a, x); //F(n)출력) return a.data[0][1]; } M divide(M a, int n) { if (n > 1) { //n=1이 될때 까지 분할한다.(분할) a = divide(a, n / 2); //분할된 행렬을 다시 곱함(정복) a = multiply(a, a); //n이 홀수면 if (n % 2 == 1) { M b; b.data[0][0] = 1, b.data[0][1] = 1; b.data[1][0] = 1, b.data[1][1] = 0; //구한 a와 f(1)곱해줌. a = multiply(a, b); } } return a; } M multiply(M a, M b) { M c; //행렬곱셈 c.data[0][0] = a.data[0][0] * b.data[0][0] + a.data[0][1] * b.data[1][0]; c.data[0][1] = a.data[0][0] * b.data[0][1] + a.data[0][1] * b.data[1][1]; c.data[1][0] = a.data[1][0] * b.data[0][0] + a.data[1][1] * b.data[1][0]; c.data[1][1] = a.data[1][0] * b.data[0][1] + a.data[1][1] * b.data[1][1]; return c; }
코드가 어려울 수 있는데, 노트에 직접 적어보면 분할 정복의 과정이 보일겁니다.
결과