Contacte

Găsim numărul N de Fibonacci în trei moduri pentru un timp acceptabil: elementele de bază ale programului dinamic. Numerele Fibonacci: ciclul Fibonacci și recursionarea recursurilor

Foarte adesea pe o varietate de olimpiade, sarcinile par a fi așa, care, după cum sa gândit la prima vedere, pot fi rezolvate cu o simplă busting. Dar dacă calculează numărul opțiuni posibileMă voi asigura imediat de ineficiența acestei abordări: de exemplu, o simplă funcție recursivă de mai jos va consuma resurse substanțiale pe data de 30 de fibonacci, în timp ce la Jocurile Olimpice, timpul de decizie este adesea limitat la 1-5 secunde.

INT FIBO (INT N) (dacă n \u003d\u003d 1 || n \u003d\u003d 2) (retur 1;) (Return Fibo (N-1) + fibo (N-2)))

Să ne gândim la ce se întâmplă. De exemplu, pentru a calcula fibo (30), calculează mai întâi fibo (29) și fibo (28). Dar, în același timp, programul nostru "uită" că fibo (28) noi deja calculată Când căutați fibo (29).

Eroarea principală a acestei abordări "în frunte" este că aceleași valori ale argumentelor funcției sunt calculate în mod repetat - și aceasta este o operațiune suficientă de resurse. Scapă de calcule repetitive ne vor ajuta programare dinamică - Aceasta este o recepție, atunci când se utilizează pe care sarcina este împărțită în subtansk-uri comune și repetate, fiecare dintre acestea fiind rezolvată doar 1 timp - aceasta îmbunătățește semnificativ eficiența programului. Această metodă este descrisă în detaliu, există și exemple de rezolvare a altor sarcini.

Cea mai ușoară opțiune de îmbunătățire a funcției noastre este de a memora ce valori am calculat deja. Pentru a face acest lucru, trebuie să introduceți o matrice suplimentară care va servi drept "cache" pentru calculele noastre: Înainte de a calcula noua valoare, vom verifica dacă a fost calculată înainte. Dacă s-au calculat, vom lua o valoare gata făcută de la matrice și, dacă nu este calculată - va trebui să o considerați pe baza celor anterioare și să memorați pentru viitor:

Int cache; int fibo (int n) (dacă (cache [n] \u003d\u003d 0) (dacă (n \u003d\u003d 1 || n \u003d\u003d 2) (cache [n] \u003d 1;) altceva (cache [n] \u003d fibo (n - 1) + fibo (N-2);)) cache cache [n];)

Deoarece în această sarcină de a calcula valoarea n-minții, vom fi garantați pentru a fi garantate (n-1) -e, nu va fi dificil să rescrieți formula într-o formă iterativă - pur și simplu vom completa matricea noastră într-o rând până când vine vorba de celula dorită:

<= n; i++) { cache[i] = cache + cache; } cout << cache;

Acum putem observa că atunci când calculează valoarea F (n), valoarea F (n-3) este deja garantată nu nu va avea nevoie. Adică, este suficient să stocăm doar două valori în memoria - f (n - 1) și f (n-2). Mai mult, de îndată ce am calculat f (n), stocarea f (n-2) pierde orice înțeles. Să încercăm să scriem aceste reflecții sub formă de cod:

// două valori anterioare: int cache1 \u003d 1; int cache2 \u003d 1; // noua valoare int cache3; pentru (int i \u003d 2; eu<= n; i++) { cache3 = cache1 + cache2; //Вычисляем новое значение //Абстрактный cache4 будет равен cache3+cache2 //Значит cache1 нам уже не нужен?.. //Отлично, значит cache1 -- то значение, которое потеряет актуальность на следующей итерации. //cache5 = cache4 - cache3 => Prin intermediul iterației va pierde relevanța cache2, adică. Ar trebui să fie cache1 // cu alte cuvinte, cache1 - F (N-2), cache2 - F (N-1), cache3 - F (n). // Fie n \u003d n + 1 (numărul pe care îl calculam în următoarea iterație). Apoi N-2 \u003d N-3, N-1 \u003d N-2, N \u003d N-1. // În conformitate cu noile realități, rescriu valorile variabilelor noastre: cache1 \u003d cache2; cache2 \u003d cache3; ) Cout<< cache3;

Programatorul experimentat este clar că codul este mai mare, în general, prostii, deoarece cache-ul3 nu este utilizat niciodată (este înregistrat imediat în cache2) și puteți rescrie toată repetarea utilizând doar o singură expresie:

Cache \u003d 1; cache \u003d 1; pentru (int i \u003d 2; i<= n; i++) { cache = cache + cache; //При i=2 устареет 0-й элемент //При i=3 в 0 будет свежий элемент (обновили его на предыдущей итерации), а в 1 -- ещё старый //При i=4 последним элементом мы обновляли cache, значит ненужное старьё сейчас в cache //Интуитивно понятно, что так будет продолжаться и дальше } cout << cache;

Pentru cei care nu pot înțelege cum funcționează magia cu reziduul din diviziune, sau doar dorește să vadă o formulă mai non-evidentă, există o altă soluție:

Int x \u003d 1; int y \u003d 1; pentru (int i \u003d 2; eu< n; i++) { y = x + y; x = y - x; } cout << "Число Фибоначчи: " << y;

Încercați să urmați executarea acestui program: veți fi sigur că ați corectat algoritmul.

P.S. În general, există o singură formulă pentru calcularea oricărui număr de Fibonacci, care nu necesită nici o iterație sau recursură:

Const dublu sqrt5 \u003d sqrt (5); Const dublu phi \u003d (sqrt5 + 1) / 2; INT FIBO (INT N) (Return INT (PHI, N) / SQRT5 + 0.5);)

Dar, după cum puteți ghici, captura este că prețul de calcul al gradelor numerelor neuropale este destul de mare, după cum este eroarea lor.

Numerele Fibonacci - Acesta este un număr de numere în care fiecare număr următor este egal cu suma celor două precedente: 1, 1, 2, 3, 5, 8, 13, .... Uneori, un rând începe de la zero: 0, 1, 1, 2, 3, 5, .... În acest caz, vom adera la prima opțiune.

Formulă:

F 1 \u003d 1
F 2 \u003d 1
F n \u003d f n-1 + f n-2

Exemplu de calcul:

F 3 \u003d F 2 + F 1 \u003d 1 + 1 \u003d 2
F 4 \u003d F 3 + F 2 \u003d 2 + 1 \u003d 3
F 5 \u003d F 4 + F 3 \u003d 3 + 2 \u003d 5
F 6 \u003d F 5 + F 4 \u003d 5 + 3 \u003d 8
...

Calcularea numărului N-Th a rândului Fibonacci folosind ciclul de timp

  1. Atribuiți primele elemente ale seriei la variabilele FIB1 și FIB2, adică o unitate la o variabilă.
  2. Solicitați numărul de utilizator al elementului a cărui valoare dorește să obțină. Atribuiți numărul variabilei n.
  3. Efectuați următoarele acțiuni n - de 2 ori, deoarece primele două elemente sunt deja luate în considerare:
    1. Fix FIB1 și FIB2 prin atribuirea rezultatului unei variabile pentru stocarea temporară a datelor, de exemplu, FIB_SUM.
    2. Variabila FIB1 Atribuiți valoarea FIB2.
    3. Variabila FIB2 Atribuiți valoarea FIB_SUM.
  4. Afișați FIB2.

Notă. Dacă utilizatorul intră 1 sau 2, corpul ciclului nu este niciodată efectuat, este afișată valoarea original FIB2.

fib1 \u003d 1 fib2 \u003d 1 n \u003d intrare () n \u003d int (n) i \u003d 0 în timp ce eu< n - 2 : fib_sum = fib1 + fib2 fib1 = fib2 fib2 = fib_sum i = i + 1 print (fib2)

Opțiune de cod compactă:

fIB1 \u003d FIB2 \u003d 1 N \u003d INT (intrare ( "Numărul elementului unui rând de Fibonacci:")) - 2 în timp ce N\u003e 0: Fib1, Fib2 \u003d Fib2, FIB1 + FIB2N N- \u003d 1 Imprimare (FIB2)

Ieșirea numerelor Fibonacci cu un ciclu pentru

În acest caz, nu numai valoarea elementului de fundație al unei serii de Fibonacci este afișată, dar și toate numerele către acesta incluzive. Pentru a face acest lucru, ieșirea valorii FIB2 este plasată în ciclu.

fib1 \u003d fib2 \u003d 1 n \u003d int (intrare ()) dacă n< 2 : quit() print (fib1, end= " " ) print (fib2, end= " " ) for i in range (2 , n) : fib1, fib2 = fib2, fib1 + fib2 print (fib2, end= " " ) print ()

Exemplu de execuție:

10 1 1 2 3 5 8 13 21 34 55

Calculul recursiv al numărului N-Th a rândului Fibonacci

  1. Dacă n \u003d 1 sau n \u003d 2, reveniți la unitatea de ramură apelată, deoarece primul și al doilea element al gamei Fibonacci sunt egale cu unul.
  2. În toate celelalte cazuri, cauzează aceeași funcție cu argumentele N-1 și N - 2. Rezultatul a două apeluri pentru a se plia și a reveni la ramura apelantului programului.

dEF FIBONACCI (N): Dacă n în (1, 2): retur 1 retur Fibonacci (N-1) + Fibonacci (N-2) Imprimare (Fibonacci (10))

Să presupunem n \u003d 4. atunci va fi un apel recursiv Fibonacci (3) și Fibonacci (2). Al doilea va returna o unitate, iar prima va duce la încă două provocări ale funcției: Fibonacci (2) și Fibonacci (1). Ambele apeluri vor fi returnate de unul, în suma vor fi două. Astfel, apelul Fibonacci (3) returnează numărul 2, care este rezumat cu un număr 1 din apelul Fibonacci (2). Rezultatul 3 revine la ramura principală a programului. Al patrulea element al gama Fibonacci este de trei: 1 1 2 3.

Programatorii numărului Fibonacci trebuie deja să-și plăcească. Exemple de calcule ale acestora sunt utilizate peste tot. Totul din faptul că aceste numere oferă cel mai simplu exemplu de recursură. Și ele sunt un bun exemplu de programare dinamică. Dar este necesar să le calculați astfel în proiectul real? Nu face. Nici recursiunea, nici programul dinamic nu este opțiunile ideale. Și formula non-închisă care utilizează numere de puncte plutitoare. Acum vă voi spune cât de corect. Dar mai întâi treceți prin toate soluțiile bine-cunoscute.

Codul este conceput pentru Python 3, deși trebuie să meargă pe Python 2.

Pentru a începe cu - Am reamintesc definiția:

F n \u003d f n-1 + f n-2

Și F 1 \u003d F 2 \u003d 1.

Formula închisă

Să pierdem detaliile, dar cei care doresc să se familiarizeze cu încheierea formulei. Ideea este de a presupune că există un anumit x pentru care F N \u003d x N, apoi găsiți x.

Ce înseamnă

Reducerea x N-2

Rezolvăm ecuația pătrată:

De unde este cultivarea "secțiunea de aur" φ \u003d (1 + √5) / 2. Înlocuirea valorilor inițiale și făcând mai multă calcul, obținem:

Pe măsură ce folosim pentru a calcula f n.

De la __future__ Diviziunea de import Import Math Def (N): SQRT5 \u003d Math.SQRT (5) Phi \u003d (SQRT5 + 1) / 2 Return INT (Phi ** N / sqrt5 + 0.5)

Bun:
Rapid și doar pentru micul n
Săraci:
V-a dorit operațiuni de virgulă plutitoare. Pentru o mare n, va fi necesară o mare precizie.
Rău:
Utilizarea numerelor integrate pentru a calcula F N este frumos dintr-un punct de vedere matematic, dar urât - cu un computer.

Recursură

Cea mai evidentă decizie pe care ați văzut-o deja de multe ori - cel mai probabil, ca exemplu în ceea ce privește recursiunea. Îl repet din nou pentru completitudine. În Python, acesta poate fi scris într-o singură linie:

Fib \u003d lambda n: fib (n - 1) + fib (n - 2) dacă n\u003e 2 altceva 1

Bun:
Implementarea foarte simplă Repetarea definiției matematice
Săraci:
Timp de execuție exponențială. Pentru că n mare foarte încet
Rău:
Stack se revarsă

Memorie

Soluția cu recursiune are o mare problemă: intersectarea calculelor. Când se numește Fib (N), se calculează FIB (N-1) și FIB (N-2). Dar când se consideră fibul (N-1), acesta va calcula independent FIB (N-2) - adică FIB (N-2) va fi calculat de două ori. Dacă continuați argumentele, se va vedea că fibul (n-3) va fi calculat de trei ori etc. Prea multe intersecții.

Prin urmare, trebuie doar să memorați rezultatele pentru a nu le număra din nou. Timpul și memoria acestei soluții sunt cheltuite liniar. În rezolvarea am un dicționar, dar ați putea folosi o matrice simplă.

M \u003d (0: 0, 1: 1) DEF FIB (N): Dacă n în m: retur m [n] m [n] \u003d fib (n-1) + fib (n-2) retur m [n]

(În Python, se poate face și folosind un decorator, functools.lu_cache.)

Bun:
Doar întoarceți recursionarea într-o soluție de memorie. Transformă timpul exponențial pentru a executa în liniară, pentru care cheltuiește mai multă memorie.
Săraci:
Petrece o mulțime de memorie
Rău:
Este posibil ca stiva de depășire, ca și în recurs

Programare dinamică

După decizia cu memorarea devine clar că nu avem nevoie de toate rezultatele anterioare, ci doar ultimele două. În plus, în loc să porniți cu FIB (N) și să vă întoarceți, puteți începe cu FIB (0) și mergeți înainte. Următorul cod are o execuție liniară de timp, iar utilizarea memoriei este fixată. În practică, viteza soluției va fi chiar mai mare, deoarece nu există provocări recursive ale funcțiilor și operațiunii asociate. Și codul pare mai ușor.

Această soluție este adesea adusă ca exemplu de programare dinamică.

DEF FIB (N): A \u003d 0 B \u003d 1 pentru __ în intervalul (N): A, B \u003d B, A + B Returnarea a

Bun:
Funcționează rapid pentru un cod simplu, simplu
Săraci:
Timp de execuție liniar
Rău:
Da, nimic nu este nimic.

Matrix Algebra.

Și în cele din urmă, cea mai puțin iluminată, dar cea mai corectă soluție, folosind timp și memorie. De asemenea, poate fi extins pe orice secvență liniară omogenă. Ideea utilizării matricelor. Este suficient de ușor să vezi asta

Și generalizarea spune asta

Două valori pentru x, obținute de noi mai devreme, dintre care una reprezentau o secțiune de aur, sunt valoroase de matrice. Prin urmare, un alt mod de ieșire a unei formule închise este utilizarea unei ecuații matrice și a unei algebre liniare.

Deci, ce este utilă o astfel de formulare? Prin faptul că expoziția poate fi efectuată pentru timpul logaritmic. Acest lucru se face prin construirea pătratului. Linia de jos este aceea

În cazul în care prima expresie este folosită pentru o, a doua oală pentru ciudat. Rămâne numai pentru a organiza multiplicarea matricelor și totul este gata. Se obține următorul cod. Am organizat o implementare recursivă a POW, deoarece este mai ușor de înțeles. Versiunea iterativă arată aici.

DEF POW (X, N, I, Multe): "" "Returnează X la gradul N. Se presupune că sunt o singură matrice care variază cu mult și n este un întreg" "" dacă n \u003d\u003d 0: întoarce I Elif n \u003d\u003d 1: Întoarceți X ELSE: Y \u003d POW (x, N // 2, I, Mult) y \u003d mai mult (y, y) dacă n% 2: y \u003d mult (x, y) Return y def IDENTITITE_MARIX (N): "" "Returnează o singură matrice n on n" "R \u003d lista (intervalul (N)) RETURN [pentru j în r] def matrix_multiply (A, B): bt \u003d lista (ZIP (* B )) Returnați [pentru Row_a într-un] Def (N): F \u003d POW ([,], N, Identity_matrix (2), Matrix_multiply) Return F

Bun:
Memorie fixă, timp logaritmic
Săraci:
Codul este mai complicat
Rău:
Trebuie să lucreze cu matricele, deși nu sunt atât de rele

Compararea vitezei

Este doar o variantă de programare și matrice dinamică. Dacă le compară cu numărul de caractere, printre n, se pare că soluția matricei este liniar și soluția cu programare dinamică este exponențială. Exemplu practic - calculul FIB (10 ** 6), numărul care va avea mai mult de două sute de mii de caractere.

N \u003d 10 ** 6
Calculați Fib_matrix: FIB (N) are doar 208988 cifre, calculul a durat 0,24993 secunde.
Calculați Fib_dynamic: FIB (N) este de numai 208988 cifre, calculul a durat 11,83377 secunde.

Comentarii teoretice

Nu atingeți direct codul de mai sus, această observație este încă un interes. Luați în considerare următorul grafic:

Calculați numărul de căi N de la A la B. De exemplu, pentru n \u003d 1 avem o modalitate, 1. Pentru n \u003d 2, avem din nou într-un fel, 01. Pentru n \u003d 3 avem două moduri, 001 și 101 . Este destul de simplu să se arate că numărul de căi N de la A la B este egal cu acuratețea f n. După ce scrieți de pe matricea aranjamentului pentru grafic, obținem aceeași matrice care a fost descrisă mai sus. Acesta este un rezultat bine cunoscut din teoria graficelor, care pentru o matrice dată de adianță A, apariția C N este numărul de căi n în coloană (una dintre sarcinile menționate în filmul "Urmenssa va vâna" ).

De ce există astfel de denumiri pe roam-uri? Se pare că atunci când se analizează o secvență infinită de caractere în ambele părți ale secvenței de căi de pe coloană, veți obține ceva numit "Schimbări de tip final", care este un tip de sistem de difuzoare simbolic. În mod specific, acest tip de tip final este cunoscut sub numele de "Schimbarea secțiunii de aur" și este stabilită ca un set de "cuvinte interzise" (11). Cu alte cuvinte, vom obține secvențe binare infinite în ambele direcții și nici o perechi de ele nu vor fi adiacente. Entropia topologică a acestui sistem dinamic este egală cu secțiunea de aur φ. Mă întreb cum acest număr apare periodic în diferite domenii de matematică.



Ți-a plăcut articolul? Împărtășește-l