Złożoność obliczeniowa sekwencji Fibonacciego


330

Rozumiem notację Big-O, ale nie wiem, jak ją obliczyć dla wielu funkcji. W szczególności starałem się zrozumieć złożoność obliczeniową naiwnej wersji sekwencji Fibonacciego:

int Fibonacci(int n)
{
    if (n <= 1)
        return n;
    else
        return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Jaka jest złożoność obliczeniowa sekwencji Fibonacciego i jak jest obliczana?



3
Zobacz sekcję formularza macierzy tutaj: en.wikipedia.org/wiki/Fibonacci_number . wykonując tę ​​macierz ^ n (w sprytny sposób) możesz obliczyć Fib (n) w O (lg n). Sztuką jest wykonywanie funkcji zasilania. Jest bardzo dobry wykład na iTunesU na temat tego dokładnie problemu i jak go rozwiązać w O (lg n). Kurs jest wstępem do algorytmów z wykładu MIT 3 (jest absolutnie darmowy, więc sprawdź go, jeśli jesteś zainteresowany)
— Aly

1
Żaden z powyższych komentarzy nie dotyczy pytania, które dotyczy złożoności obliczeniowej wersji naiwnej (w opublikowanym kodzie), a nie bardziej inteligentnych wersji, takich jak forma matrycowa lub obliczenia nierekurencyjne.
— Josh Milthorpe

Bardzo fajne wideo , które mówi o złożoności dolnej granicy (2 ^ n / 2) i złożoności górnej granicy (2 ^ n) implementacji rekurencyjnej.
— RBT

1
Dodatkowe pytanie: czy naiwna implementacja serii Fibonacciego powinna być iteracyjna czy rekurencyjna ?
— RBT

Odpowiedzi:


374

Modelujesz funkcję czasu do obliczenia Fib(n)jako sumę czasu do obliczenia Fib(n-1)plus czas do obliczenia Fib(n-2)plus czas do ich dodania ( O(1)). Fib(n)Zakłada się, że powtórne oceny tego samego zajmują ten sam czas - tzn. Nie stosuje się zapamiętywania.

T(n<=1) = O(1)

T(n) = T(n-1) + T(n-2) + O(1)

Rozwiązujesz tę relację powtarzalności (na przykład używając funkcji generujących) i otrzymujesz odpowiedź.

Alternatywnie możesz narysować drzewo rekurencji, które będzie miało głębokość ni intuicyjnie zorientuje się, że ta funkcja jest asymptotycznie . Następnie możesz udowodnić swoje przypuszczenie poprzez indukcję.O(2n)

Baza: n = 1jest oczywista

Zakładamy , dlategoT(n-1) = O(2n-1)

T(n) = T(n-1) + T(n-2) + O(1) co jest równe

T(n) = O(2n-1) + O(2n-2) + O(1) = O(2n)

Jednak, jak zauważono w komentarzu, nie jest to ścisłe ograniczenie. Ciekawym faktem na temat tej funkcji jest to, że T (n) jest asymptotycznie taka sama jak wartość, Fib(n)ponieważ oba są zdefiniowane jako

f(n) = f(n-1) + f(n-2).

Liście drzewa rekurencyjnego zawsze zwracają 1. Wartość Fib(n)jest sumą wszystkich wartości zwróconych przez liście w drzewie rekurencyjnym, która jest równa liczbie liści. Ponieważ każdy liść potrzebuje O (1) do obliczenia, T(n)jest równy Fib(n) x O(1). W związku z tym ścisłym związkiem tej funkcji jest sama sekwencja Fibonacciego (~ ). Możesz dowiedzieć się o tej ścisłej więzi, korzystając z funkcji generowania, o których wspomniałem powyżej.θ(1.6n)


29
Dowód również indukcyjny. Miły. +1
— Andrew Rollings,

Chociaż granica nie jest ciasna.
— Kapitan Segfault,

@Captain Segfault: Tak. Wyjaśniłem odpowiedź. Będziesz mocno związany metodą GF, jak napisałem powyżej.
— Mehrdad Afshari,

Potraktuj swój StackOverflowException jako żart. Czas wykładniczy jest dość łatwo zauważalny przy raczej niewielkich wartościach n.
— David Rodríguez - dribeas

1
„Alternatywnie możesz narysować drzewo rekurencyjne, które będzie miało głębokość n i intuicyjnie zorientuje się, że ta funkcja ma asymptotycznie wartość O (2n).” - To jest całkowicie fałszywe. Złożoność czasu wynosi O (golden_ratio ^ n). Nigdy nie zbliża się do O (2 ^ n). Gdybyś mógł sięgnąć w nieskończoność, zbliżyłby się do O (golden_ratio ^ n). Taka jest asymptota, odległość między dwiema liniami musi zbliżyć się do 0.
— Bob

133

Wystarczy zadać sobie pytanie, ile instrukcji należy wykonać, aby F(n)je wypełnić.

Na F(1)odpowiedź jest 1(pierwsza część warunkowa).

Ponieważ F(n)odpowiedź brzmi F(n-1) + F(n-2).

Jaka funkcja spełnia te zasady? Wypróbuj n (a> 1):

a n == a (n-1) + a (n-2)

Podziel przez (n-2) :

a 2 == a + 1

Rozwiąż ai otrzymasz (1+sqrt(5))/2 = 1.6180339887, inaczej zwany złotym współczynnikiem .

Zajmuje to zatem czas wykładniczy.


8
Dowód indukcyjny. Miły. +1
— Andrew Rollings,

2
30 głosów za błędną odpowiedzią? :-) Czy wynika z tego, że 1 = F (1) = (1 + sqrt (5)) / 2? A co z drugim rozwiązaniem (1-sqrt (5)) / 2?
— Carsten S

1
Nie, 1 nie jest równe 1 + 1. Funkcja spełniająca te reguły jest wymieniona w pytaniu.
— molbdnilo

6
Odpowiedź nie jest zła. Ma rację bezobjawowo. Drugie rozwiązanie jest negatywne, więc fizycznie nie ma sensu.
— Da Teng

10
Czy ktoś może wyjaśnić, w jaki sposób ^ n == a ^ (n-1) + a ^ (n-2) spełnia te zasady? W jaki sposób jest dokładnie spełniony, proszę podać konkretne.
— szczery

33

Zgadzam się z pgaur i rickerbh, złożoność rekurencyjnego fibonacciego to O (2 ^ n).

Doszedłem do tego samego wniosku raczej uproszczeniem, ale nadal uważam, że jest to uzasadnione uzasadnienie.

Po pierwsze, chodzi o ustalenie, ile razy wywoływana jest funkcja rekurencyjna Fibonacciego (F () odtąd) podczas obliczania N-tej liczby Fibonacciego. Jeśli zostanie wywołany raz na numer w sekwencji od 0 do n, wówczas mamy O (n), jeśli zostanie wywołany n razy dla każdego numeru, otrzymamy O (n * n) lub O (n ^ 2), i tak dalej.

Tak więc, gdy F () jest wywoływana dla liczby n, liczba wywołań F () dla danej liczby od 0 do n-1 rośnie, gdy zbliżamy się do 0.

Na pierwszy rzut oka wydaje mi się, że jeśli ułożymy to wizualnie, narysowanie jednostki za każdym razem, gdy F () jest wywoływane dla danej liczby, mokro uzyskuje rodzaj piramidy (to znaczy, jeśli centrujemy jednostki poziomo ). Coś takiego:

n              *
n-1            **
n-2           ****  
...
2           ***********
1       ******************
0    ***************************

Pytanie brzmi: jak szybko powiększa się podstawa tej piramidy, gdy n rośnie?

Weźmy prawdziwy przypadek, na przykład F (6)

F(6)                 *  <-- only once
F(5)                 *  <-- only once too
F(4)                 ** 
F(3)                ****
F(2)              ********
F(1)          ****************           <-- 16
F(0)  ********************************    <-- 32

Widzimy, że F (0) jest wywoływany 32 razy, czyli 2 ^ 5, co w tym przypadku próbki wynosi 2 ^ (n-1).

Teraz chcemy wiedzieć, ile razy F (x) jest wywoływane, i widzimy, ile razy F (0) jest wywoływane, jest tylko częścią tego.

Jeśli przenosimy mentalnie wszystkie * z linii F (6) do linii F (2) do linii F (1), widzimy, że linie F (1) i F (0) mają teraz równą długość. Co oznacza, że ​​całkowite czasy F () są wywoływane, gdy n = 6 wynosi 2x32 = 64 = 2 ^ 6.

Teraz pod względem złożoności:

O( F(6) ) = O(2^6)
O( F(n) ) = O(2^n)

3
F (3) zostaje wywołany tylko 3 razy, a nie 4 razy. Druga piramida jest zła.
— Avik

2
F (3) = 3, F (2) = 5, F (1) = 8, F (0) = 5. Naprawiłbym to, ale nie sądzę, żebym mógł uratować tę odpowiedź za pomocą edycji.
— Bernhard Barker,

31

W MIT jest bardzo miła dyskusja na temat tego konkretnego problemu . Na stronie 5 podkreślają, że jeśli założymy, że suma zajmuje jedną jednostkę obliczeniową, czas wymagany do obliczenia Fib (N) jest bardzo ściśle związany z wynikiem Fib (N).

W rezultacie możesz przejść bezpośrednio do bardzo zbliżonego przybliżenia serii Fibonacciego:

Fib(N) = (1/sqrt(5)) * 1.618^(N+1) (approximately)

i powiedzmy zatem, że najgorszym przypadkiem naiwnego algorytmu jest

O((1/sqrt(5)) * 1.618^(N+1)) = O(1.618^(N+1))

PS: Dyskusja na temat wyrażenia w formie zamkniętej liczby N-tego Fibonacciego na Wikipedii, jeśli chcesz uzyskać więcej informacji.


Dzięki za link do kursu. Bardzo ładna obserwacja
— SwimBikeRun

16

Możesz go rozwinąć i mieć wizualizację

     T(n) = T(n-1) + T(n-2) <
     T(n-1) + T(n-1) 

     = 2*T(n-1)   
     = 2*2*T(n-2)
     = 2*2*2*T(n-3)
     ....
     = 2^i*T(n-i)
     ...
     ==> O(2^n)

1
Rozumiem pierwszą linię. Ale dlaczego <na końcu jest mniej niż postać ? Jak się dostałeś T(n-1) + T(n-1)?
— Quazi Irfan

@QuaziIrfan: D to strzała. -> [(nie mniej niż). Przepraszam za zamieszanie dotyczące ostatniej linii]. W pierwszym wierszu, cóż ... T(n-1) > T(n-2)Więc mogę zmienić T(n-2)i umieścić T(n-1). Dostanę tylko wyższą granicę, która nadal obowiązujeT(n-1) + T(n-2)
— Tony Tannous,

10

Jest on ograniczony na dolnym końcu 2^(n/2)i na górnym końcu przez 2 ^ n (jak zauważono w innych komentarzach). Ciekawym faktem tej rekurencyjnej implementacji jest to, że ma ona ścisłą asymptotyczną granicę samego Fib (n). Fakty te można streścić:

T(n) = Ω(2^(n/2))  (lower bound)
T(n) = O(2^n)   (upper bound)
T(n) = Θ(Fib(n)) (tight bound)

Jeśli chcesz, ciasne ograniczenie można jeszcze bardziej zmniejszyć za pomocą zamkniętej formy .


10

Odpowiedzi na dowody są dobre, ale zawsze muszę wykonać kilka iteracji ręcznie, aby naprawdę się przekonać. Narysowałem więc małe drzewo wywołujące na mojej tablicy i zacząłem liczyć węzły. Dzielę swoje liczby na wszystkie węzły, węzły liści i węzły wewnętrzne. Oto co mam:

IN | OUT | TOT | LEAF | INT
 1 |   1 |   1 |   1  |   0
 2 |   1 |   1 |   1  |   0
 3 |   2 |   3 |   2  |   1
 4 |   3 |   5 |   3  |   2
 5 |   5 |   9 |   5  |   4
 6 |   8 |  15 |   8  |   7
 7 |  13 |  25 |  13  |  12
 8 |  21 |  41 |  21  |  20
 9 |  34 |  67 |  34  |  33
10 |  55 | 109 |  55  |  54

To, co natychmiast się wyskakuje, to liczba węzłów liści fib(n). Zauważyłem jeszcze kilka iteracji, że liczba wewnętrznych węzłów jest fib(n) - 1. Dlatego całkowita liczba węzłów wynosi 2 * fib(n) - 1.

Ponieważ upuść współczynniki przy klasyfikacji złożoności obliczeniowej, ostateczna odpowiedź θ(fib(n)).


(Nie, nie narysowałem pełnego 10-
— poziomowego

Fajnie, zastanawiałem się, ile dodatkowych dodatków rekurencyjny Fib zrobił. To nie tylko dodawanie 1do jednego akumulatora Fib(n)razy, ale interesujące, że wciąż jest dokładnie θ(Fib(n)).
— Peter Cordes

Zauważ, że niektóre (najbardziej) rekurencyjne implementacje poświęcają czas na dodawanie 0, jednak: podstawowymi przypadkami rekurencji są 0i 1, ponieważ tak się dzieje Fib(n-1) + Fib(n-2). Prawdopodobnie odpowiedź 3 * Fib(n) - 2z tej tylko linku jest dokładniejsza dla całkowitej liczby węzłów, a nie 2 * Fib(n) - 1.
— Peter Cordes

Nie mogę uzyskać takich samych wyników w węzłach liści. Począwszy od 0: F (0) -> 1 liść (sam); F (1) -> 1 liść (sam); F (2) -> 2 liście (F (1) i F (0)); F (3) -> 3 liście; F (5) -> 8 liści; itp.
— alexlomba87

9

Złożoność czasową algorytmu rekurencyjnego można lepiej oszacować przez narysowanie drzewa rekurencyjnego. W tym przypadku relacja powtarzalności dla narysowanego drzewa rekurencyjnego wynosiłaby T (n) = T (n-1) + T (n-2) + O (1) zauważ, że każdy krok wymaga O (1), co oznacza stały czas, ponieważ wykonuje tylko jedno porównanie, aby sprawdzić wartość nw bloku, jeśli drzewo rekurencji wyglądałoby jak

          n
   (n-1)      (n-2)
(n-2)(n-3) (n-3)(n-4) ...so on

Powiedzmy tutaj, że każdy poziom powyższego drzewa jest oznaczony przez:

i
0                        n
1            (n-1)                 (n-2)
2        (n-2)    (n-3)      (n-3)     (n-4)
3   (n-3)(n-4) (n-4)(n-5) (n-4)(n-5) (n-5)(n-6)

powiedzmy, że przy określonej wartości i drzewo kończy się, taki przypadek miałby miejsce, gdy ni = 1, stąd i = n-1, co oznacza, że ​​wysokość drzewa wynosi n-1. Zobaczmy teraz, ile pracy wykonano dla każdej z n warstw w drzewie. Zauważ, że każdy krok zajmuje czas O (1), jak podano w relacji powtarzalności.

2^0=1                        n
2^1=2            (n-1)                 (n-2)
2^2=4        (n-2)    (n-3)      (n-3)     (n-4)
2^3=8   (n-3)(n-4) (n-4)(n-5) (n-4)(n-5) (n-5)(n-6)    ..so on
2^i for ith level

ponieważ i = n-1 to wysokość pracy drzewa wykonanej na każdym poziomie będzie

i work
1 2^1
2 2^2
3 2^3..so on

Stąd suma wykonanej pracy będzie sumą pracy wykonanej na każdym poziomie, stąd będzie to 2 ^ 0 + 2 ^ 1 + 2 ^ 2 + 2 ^ 3 ... + 2 ^ (n-1), ponieważ i = n-1. Według szeregów geometrycznych suma ta wynosi 2 ^ n, stąd całkowita złożoność czasu wynosi O (2 ^ n)


2

Według mnie jest O(2^n)tak, ponieważ w tej funkcji tylko rekurencja zajmuje dużo czasu (dziel i zwyciężaj). Widzimy, że powyższa funkcja będzie kontynuowana w drzewie, dopóki liście nie zbliżą się, gdy osiągniemy poziom F(n-(n-1))tj F(1). Tak więc tutaj, gdy zanotujemy złożoność czasową występującą na każdej głębokości drzewa, seria podsumowań jest następująca:

1+2+4+.......(n-1)
= 1((2^n)-1)/(2-1)
=2^n -1

to jest kolejność 2^n [ O(2^n) ].


1

Naiwna rekurencyjna wersja Fibonacciego jest z założenia wykładnicza ze względu na powtarzalność w obliczeniach:

W katalogu głównym obliczasz:

F (n) zależy od F (n-1) i F (n-2)

F (n-1) ponownie zależy od F (n-2) i F (n-3)

F (n-2) ponownie zależy od F (n-3) i F (n-4)

wtedy na każdym poziomie są 2 wywołania rekurencyjne, które marnują dużo danych w obliczeniach, funkcja czasu będzie wyglądać następująco:

T (n) = T (n-1) + T (n-2) + C, ze stałą C.

T (n-1) = T (n-2) + T (n-3)> T (n-2) następnie

T (n)> 2 * T (n-2)

...

T (n)> 2 ^ (n / 2) * T (1) = O (2 ^ (n / 2))

Jest to tylko dolna granica, która do celów analizy powinna wystarczyć, ale funkcja czasu rzeczywistego jest współczynnikiem stałej według tej samej formuły Fibonacciego, a forma zamknięta jest znana jako wykładniczy złoty współczynnik.

Ponadto można znaleźć zoptymalizowane wersje Fibonacciego za pomocą programowania dynamicznego, takiego jak to:

static int fib(int n)
{
    /* memory */
    int f[] = new int[n+1];
    int i;

    /* Init */
    f[0] = 0;
    f[1] = 1;

    /* Fill */
    for (i = 2; i <= n; i++)
    {
        f[i] = f[i-1] + f[i-2];
    }

    return f[n];
}

Jest to zoptymalizowane i wykonuje tylko n kroków, ale ma również charakter wykładniczy.

Funkcje kosztów są definiowane od wielkości wejściowej do liczby kroków w celu rozwiązania problemu. Gdy zobaczysz dynamiczną wersję Fibonacciego ( n kroków do obliczenia tabeli) lub najłatwiejszy algorytm do ustalenia, czy liczba jest liczbą pierwszą ( sqrt (n) do analizy poprawnych dzielników liczby). możesz myśleć, że te algorytmy to O (n) lub O (sqrt (n)), ale to po prostu nieprawda z następującego powodu: Dane wejściowe do Twojego algorytmu to liczba: n , używając notacji binarnej wielkość wejściowa dla liczba całkowita n to log2 (n), a następnie zmiana zmiennej

m = log2(n) // your real input size

poznajmy liczbę kroków w zależności od wielkości wejściowej

m = log2(n)
2^m = 2^log2(n) = n

koszt algorytmu w zależności od wielkości wejściowej wynosi:

T(m) = n steps = 2^m steps

i dlatego koszt jest wykładniczy.


1

Można to łatwo obliczyć za pomocą wywoływania funkcji diagramu. Po prostu dodaj wywołania funkcji dla każdej wartości n i sprawdź, jak rośnie liczba.

Big O oznacza O (Z ^ n), gdzie Z jest złotym stosunkiem lub około 1,62.

Zarówno liczby Leonardo, jak i liczby Fibonacciego zbliżają się do tego współczynnika, gdy zwiększamy n.

W przeciwieństwie do innych pytań Big O, zmienność danych wejściowych nie jest zmienna, a zarówno algorytm, jak i implementacja algorytmu są jasno określone.

Nie ma potrzeby stosowania złożonej matematyki. Po prostu wykreśl poniższe wywołania funkcji i dopasuj funkcję do liczb.

Lub jeśli znasz złoty podział, rozpoznasz go jako taki.

Ta odpowiedź jest bardziej poprawna niż odpowiedź zaakceptowana, która twierdzi, że zbliży się do f (n) = 2 ^ n. Nigdy nie będzie. Zbliża się do f (n) = golden_ratio ^ n.

2 (2 -> 1, 0)

4 (3 -> 2, 1) (2 -> 1, 0)

8 (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
            (2 -> 1, 0)


14 (5 -> 4, 3) (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
            (2 -> 1, 0)

            (3 -> 2, 1) (2 -> 1, 0)

22 (6 -> 5, 4)
            (5 -> 4, 3) (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
                        (2 -> 1, 0)

                        (3 -> 2, 1) (2 -> 1, 0)

            (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
                        (2 -> 1, 0)

1
Czy możesz podać jakieś źródło roszczenia dotyczącego złotego podziału?
— Nico Haase

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.