Fibonačijev niz brojeva



Fibonačijev niz je dobio naziv po italijanskom matematičaru Leonardu iz Pize, poznatijem kao Fibonači (Fibonacci). Ako znamo prva dva člana niza:

  • F(0) = 0
  • F(1) = 1

svaki sledeći član dobija se kao zbir dva prethodna:

F(n) = F(n-1) + F(n-2),  za n ≥ 2

Početak Fibonačijevog niza:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...

Fibonačijevi brojevi u prirodi

Fibonačijevi brojevi se često sreću u prirodi. Broj latica mnogih cvetova (3, 5, 8, 13, ...), kao i broj spirala na suncokretu, šišarki i ananasu najčešće su uzastopni Fibonačijevi brojevi. Slično važi i za raspored listova na grani.

Zanimljiv primer je i rodoslov pčela: trut (mužjak) ima samo majku, a ženka ima oba roditelja. Kada se prebroje preci jednog truta po generacijama, dobijaju se brojevi 1, 1, 2, 3, 5, 8, ... Količnik dva uzastopna Fibonačijeva broja približava se zlatnom preseku φ ≈ 1,618.

Fibonačijev niz i priroda - spirala školjke
Slika 1: Spirala u prirodi

Geometrijski se niz može prikazati popločavanjem kvadratima čije su stranice uzastopni Fibonačijevi brojevi:

Popločavanje kvadratima čije su stranice uzastopni Fibonačijevi brojevi
Slika 2: Popločavanje kvadratima čije su stranice uzastopni Fibonačijevi brojevi
Fibonačijeva spirala
Slika 3: Fibonačijeva spirala

Određivanje n-tog Fibonačijevog broja rekurzijom

Definicija niza je prirodno rekurzivna: da bismo izračunali F(n), funkcija mora ponovo da pozove samu sebe za n−1 i n−2.

#include <iostream>
using namespace std;

// Vraća n-ti Fibonačijev broj: F(0) = 0, F(1) = 1, F(n) = F(n-1) + F(n-2)
int fib(int n)
{
    if (n <= 1)          // bazni slučaj: F(0) = 0, F(1) = 1
        return n;

    int f1 = fib(n - 1); // rekurzivni poziv za prethodni član
    int f2 = fib(n - 2); // rekurzivni poziv za član pre njega
    return f1 + f2;
}

int main()
{
    int N, x;
    cin >> N;

    x = fib(N);
    cout << x << endl;

    return 0;
}

Na slici 4 prikazani su pozivi funkcije pri računanju F(4). Isti podproblemi računaju se više puta (na primer, F(2) dva puta), a broj poziva raste eksponencijalno sa n. Zato je ovo rešenje sporo već za n oko 40.

Određivanje n-tog elementa Fibonačijevog niza rekurzijom - pozivi funkcija
Slika 4: Stablo rekurzivnih poziva za F(4)

Određivanje n-tog Fibonačijevog broja dinamičkim programiranjem

Da bismo skratili vreme izvršavanja, pamtimo u nizu vrednosti koje su već izračunate. Nema rekurzije: pomoću for petlje računamo svaki član sabiranjem njegova dva prethodnika, zapamćena u nizu f.

#include <iostream>
#include <vector>
using namespace std;

// Svaki član računamo samo jednom i pamtimo ga u nizu f
long long fib(int n)
{
    vector<long long> f(n + 2);   // n + 2 da bi f[1] postojao i za n = 0
    f[0] = 0;
    f[1] = 1;

    for (int i = 2; i <= n; i++)
    {
        f[i] = f[i - 1] + f[i - 2];   // zbir dva prethodna, već izračunata člana
    }

    return f[n];
}

int main()
{
    int N;
    cin >> N;

    cout << fib(N) << endl;

    return 0;
}

Svaki član se računa tačno jednom, pa je složenost O(n) vremena i O(n) memorije.


Optimizovana metoda: pamćenje samo dva člana

Za računanje sledećeg člana potrebna su samo dva poslednja, pa ne moramo da čuvamo ceo niz:

#include <iostream>
using namespace std;

// Pamtimo samo dva poslednja člana niza
long long fib(int n)
{
    if (n < 2)          // F(0) = 0, F(1) = 1; petlja se tada ne bi izvršila
        return n;

    long long a = 0;    // F(i - 2)
    long long b = 1;    // F(i - 1)
    long long f = 0;    // F(i)

    for (int i = 2; i <= n; i++)
    {
        f = a + b;      // novi član
        a = b;          // pomeramo "prozor" za jedno mesto
        b = f;
    }

    return f;
}

int main()
{
    int N;
    cin >> N;

    cout << fib(N) << endl;

    return 0;
}

Na početku su prethodnici a i b postavljeni na 0 i 1. Kada se odredi novi član f = a + b, pripremaju se prethodnici za sledeći korak: prvi prethodnik postaje dosadašnji drugi (a = b), a drugi postaje upravo izračunati član (b = f). Memorija je sada O(1).

Na šta treba paziti
  • Za n = 0 i n = 1 petlja se ne izvršava, pa promenljiva f mora da ima vrednost ili ti slučajevi moraju posebno da se obrade (if (n < 2) return n;). Bez toga funkcija vraća slučajnu vrednost iz memorije.
  • Fibonačijevi brojevi brzo rastu. U tip int staje najviše F(46), a u long long najviše F(92) = 7540113804746346429. Povratni tip funkcije mora biti isti kao tip u kome se računa.

Vremenska i prostorna složenost

  • Rekurzija: vreme eksponencijalno (oko φ^n poziva)
  • Dinamičko programiranje sa nizom: vreme O(n), memorija O(n)
  • Optimizovana metoda: vreme O(n), memorija O(1)

Ako želiš da naučiš kako se Fibonači optimizuje pomoću dinamičkog programiranja (memoizacija i tabulacija), pogledaj detaljnu lekciju: DP Fibonači — memoizacija i tabulacija.