Loading [MathJax]/jax/output/HTML-CSS/jax.js
본문 바로가기
알고리즘

피보나치 수열을 구하는 효율적인 방법

by 초특급하품 2019. 10. 14.

Fibonacci

피보나치 수열이란 첫 번째와 두 번째 항이 1이고 세 번째 항부터는 이전 두 개의 숫자를 더한 점화식을 갖는 수열이다.

Fi+1=Fi+Fi1F1=F2=1

흔히 재귀 함수나 memoization 등으로 한 번쯤 구현해봤겠지만 여기서는 속도에 집중해서 빠르게 구하는 방법들을 알아보자.

 

N번째 수 구하기

1. 무작정 연산 O(n)

피보나치수열의 정의에 맞게 초기화를 한 후 모든 항을 구한다.

int byLoop(int n) {
  int f[3] = {0,1,1};
  for(int i = 3; i <= n; i++) {
    f[i%3] = f[(i-1)%3] + f[(i-2)%3];
  }
  return f[n%3];
}

 

2. 행렬을 이용한 연산 O(logn)

피보나치수열의 정의를 다시 보면서 행렬식으로 바꿔보자.

Fi+1=Fi+Fi1Fi=Fi+0

 

여기서 각 항을 상수와 F 로 나눠서 보면 행렬의 곱으로 바꿀 수 있다.

[Fi+1Fi]=[1110][FiFi1]=[1110]i[F1F0]=[1110]i[10]

이처럼 피보나치수열을 행렬의 식으로 바꾸고 오른쪽 끝 항을 계속 내리다 보면 피보나치의 정의에 있는 F0F1까지 변환했다. 이제 단순히 행렬 계산만 하면 된다. 기존의 수열 연산과 달리 행렬의 n승 연산은 log2(n) 만에 행렬 곱셈으로 아래와 같이 빠르게 구할 수 있다.

struct Matrix {
  int n;
  vector< vector > mat;

  Matrix(int n): n(n) {
    mat.resize(n, vector(n,0));
  }

  Matrix operator*(const Matrix &r) const {
    Matrix res(n);
    for(int i = 0; i < n; i++) {
      for(int j = 0; j < n; j++) {
        for(int k = 0; k < n; k++) {
          res.mat[i][j] += mat[i][k] * r.mat[k][j];
        }
      }
    }
    return res;
  }
};

Matrix exponentiation(Matrix m, int power) {
  if(power == 0) {
    Matrix id(m.n); 
    for(int i = 0; i < m.n; i++) id.mat[i][i] = 1;
      return id;
  }
  Matrix half = exponentiation(m, power/2);
  return power % 2 ? half * half * m : half * half;
}

 

3. 일반항을 이용한 연산

Fi+1=Fi+Fi1F1=F2=1

위 두 개의 식을 이용해서 Fi 의 일반항을 구해보자.

 

α+β=1αβ=1

새로운 방정식을 유도하기 위해 위와 같은 αβ 를 정의한다.

 

Fi+1FiFi1=0Fi+1αFi=β(FiαFi1)=β2(Fi1αFi2)...=βi1(F2αF1)=βi1(1α)

 

위와 비슷하게 방정식을 또 하나 유도한다.

Fi+1FiFi1=0Fi+1βFi=α(FiβFi1)=α2(Fi1βFi2)...=αi1(F2βF1)=αi1(1β)

 

이렇게 새로운 방정식 두 개를 유도했다.

Fi+1αFi=βi1(1α)Fi+1βFi=αi1(1β)

 

위 식에서 아래식을 빼고 적절히 식을 정리하면 아래식이 된다.

Fi=1αβαi1(1β)βi1(1α)=αiβiαβ

 

이로써 Fi 의 식을 F꼴이 아닌 αβ로 이루어진 일반항으로 나타내었다.
위에 정의한 αβ는 간단한 이차방정식이므로 근을 쉽게 구할 수 있다.

α=1+52β=152

 

이를 유도했던 식에 대입하면 완성이다.

Fi=15(1+52)n(152)n

double byFormula(int n) {
  double sqrt5 = sqrt(5);
  return (1/sqrt5) * (pow((1+sqrt5)/2, n) - pow((1-sqrt5)/2, n));
}

 

주의

피보나치는 n = 48 만 되어도 integer(4byte) 범위를 초과하는 무시무시한 수열이다.
따라서 위 예제에서는 전혀 고려하지 않았지만 연산에 의한 오버플로우에 항상 주의해야 한다.

댓글