이번 알고리즘 과제 중에 변형 피보나치 수열 알고리즘이 나왔는데 과제를 풀면서 찾아보니 생각보다 피보나치 수열 알고리즘이 다양했다. 정리도 할 겸 올려놓는다.
피보나치 수열의 점화식은 다음과 같다.
F(0) = 0, F(1) = 1, F(n) = F(n - 1) + F(n - 2)
ex) 0, 1, 1, 2, 3, 5, 8, 13, 21, ...
참고로 과제 문제로 나왔던 변형 피보나치 수열의 점화식은 F(0) = F(1) = 1, F(n) = F(n - 1) + F(n - 2) + 1이었다.
시간복잡도가 지수 함수인 재귀 알고리즘
long long fibo1(int n)
{
if (n < 2)
{
return n;
}
else
{
return fibo1(n - 1) + fibo1(n - 2);
}
}
가장 기초 중의 기초인 재귀 함수를 이용한 피보나치 수열 함수이다. 다들 알다시피 중복으로 호출되는 함수가 많아서 굉장히 비효율적이다. 시간복잡도는 대략 O(2^n)
시간복잡도가 선형인 반복 알고리즘
long long fibo2(int n)
{
long long a = 0, b = 1;
long long c = a + b;
if (n < 2)
{
return n;
}
else
{
for (int i = 2; i < n; i++)
{
a = b;
b = c;
c = a + b;
}
return c;
}
}
위 재귀 알고리즘을 반복문으로 고친 것이다. 시간복잡도는 O(n)
시간복잡도가 선형인 재귀 알고리즘
long long a[100] = { 0, 1 };
long long fibo3(int n)
{
if (n < 2 || a[n] != 0)
{
return a[n];
}
else
{
a[n] = fibo3(n - 1) + fibo3(n - 2);
return a[n];
}
}
이전 재귀 알고리즘이 비효율적인 이유는 이미 계산한 부분을 중복으로 호출하기 때문이다. 따라서 이미 계산한 부분을 다른 곳에 저장해놨다가 필요할 때 꺼내쓰면 재귀문으로도 반복문급 속도를 낼 수 있다. 시간복잡도가 O(n)
이 기법을 메모이제이션이라 한다. 시간복잡도와 공간복잡도를 등가 교환한 셈이지만 전역변수의 특성상 함수 종료 후에도 저장되어 있으므로 함수를 여러 번 호출한다면 위 반복문 코드보다 나을 수 있다.
시간복잡도가 로그 함수인 알고리즘
typedef vector< vector<long long> > matrix;
matrix operator *(const matrix &a, const matrix &b)
{
int n = a.size();
matrix c(n, vector<long long>(n));
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
for (int k = 0; k < n; k++)
{
c[i][j] += a[i][k] * b[k][j];
}
}
}
return c;
}
long long fibo4(int n)
{
vector<long long> tmp(2);
matrix a;
tmp[0] = 1;
tmp[1] = 1;
a.push_back(tmp);
tmp[0] = 1;
tmp[1] = 0;
a.push_back(tmp);
matrix b;
tmp[0] = 1;
tmp[1] = 0;
b.push_back(tmp);
tmp[0] = 0;
tmp[1] = 1;
b.push_back(tmp);
if (n < 2)
{
return n;
}
else
{
for (; n > 0; n /= 2)
{
if (n & 1 == 1)
{
b = b * a;
}
a = a * a;
}
return b[0][1];
}
}
사실 이 글을 쓰게 된 이유이다. 행렬곱을 이용한 방법인데 시간복잡도가 O(log n)이다. ㄷㄷ vector를 사용했기 때문에 C++에서 vector 헤더 파일이 필요하다.
피보나치 수열을 행렬로 나타내고 정리하면 다음과 같다.
결국, 행렬의 n 제곱을 계산해야 하는데 어떤 수의 n 제곱을 log n 시간에 구하는 알고리즘이 있다. 나도 그냥 가져다 쓰거라 완벽하게 이해한 건 아니지만 간단하게 설명하자면 2^32를 구한다고 했을 때 2*2*2*2*... 이런 식으로 구하는 게 아니라 2를 제곱해서 2^2을 구하고 다시 제곱해서 2^4를 구하고 다시 제곱해서 2^8을 구하는 식으로 계산하는 것이다. 계산 과정이 32에서 log_2 32로 줄어든다.
일반항을 이용한 알고리즘
long long fibo5(int n)
{
double sqrt5 = sqrt(5);
return 1 / sqrt5 * (pow((1 + sqrt5) / 2, n) - pow((1 - sqrt5) / 2, n));
}
이건 그냥 알려진 피보나치 수열의 일반항을 이용해 푸는 것이다. 사실 일반항이 있다는 것도 검색하다가 알게 되었다. sqrt와 pow 함수를 사용했기 때문에 C에서 math.h, C++에서 cmath 헤더 파일이 필요하다.
피보나치 수열의 일반항은 아래와 같다.
sqrt와 pow 함수의 시간복잡도를 모르기 때문에 정확한 시간복잡도는 모르겠지만 아마 가장 빠르지 않을까 싶다.
'프로그래밍 > C | C++' 카테고리의 다른 글
[백준] 4673번: 셀프 넘버 (0) | 2019.10.05 |
---|---|
[백준] 1546번: 평균 (0) | 2019.09.29 |
[백준] 3052번: 나머지 (0) | 2019.09.14 |
[백준] 2920번: 음계 (0) | 2019.09.12 |
[C] 라그랑주 보간법을 이용한 달팽이 배열 출력 (0) | 2019.07.09 |
삼진 탐색 (Ternary search) (0) | 2019.07.06 |
[C++] 포식자-먹이 시뮬레이션 (0) | 2019.06.24 |
[C] 2차원 배열 회전하기 (0) | 2019.06.20 |