LA CAMPANA

C'è chi ha letto questa notizia prima di te.
Iscriviti per ricevere gli ultimi articoli.
E-mail
Nome
Cognome
Come vuoi leggere The Bell
Niente spam

Molto spesso, in varie Olimpiadi, si verificano problemi come questo, che, come sembra a prima vista, possono essere risolti con una semplice ricerca. Ma se contiamo il numero di opzioni possibili, allora ci convinceremo subito dell'inefficienza di questo approccio: ad esempio, la semplice funzione ricorsiva sottostante consumerà risorse significative già al 30 ° numero di Fibonacci, mentre alle Olimpiadi il tempo di soluzione è spesso limitato a 1-5 secondi.

Int fibo (int n) (if (n \u003d\u003d 1 || n \u003d\u003d 2) (return 1;) else (return fibo (n - 1) + fibo (n - 2);))

Pensiamo al motivo per cui sta accadendo. Ad esempio, per calcolare fibo (30), calcoliamo prima fibo (29) e fibo (28). Ma allo stesso tempo il nostro programma "dimentica" quel fibo (28) noi già calcolato quando cerchi fibo (29).

L'errore principale di questo approccio "frontale" è che gli stessi valori degli argomenti della funzione vengono calcolati molte volte - e queste sono operazioni che richiedono molte risorse. Per sbarazzarci dei calcoli ripetitivi, il metodo di programmazione dinamica ci aiuterà - questa è una tecnica quando un problema viene suddiviso in sottoattività generali e ripetitive, ognuna delle quali viene risolta solo una volta - questo aumenta significativamente l'efficienza del programma. Questo metodo è descritto in dettaglio in, ci sono anche esempi di risoluzione di altri problemi.

Il modo più semplice per migliorare la nostra funzione è ricordare quali valori abbiamo già calcolato. Per fare questo, dobbiamo introdurre un array aggiuntivo, che servirà come una sorta di "cache" per i nostri calcoli: prima di calcolare un nuovo valore, controlleremo se è stato calcolato in precedenza. Se abbiamo calcolato, prenderemo il valore finito dall'array e, se non lo abbiamo calcolato, dovremo calcolarlo in base a quelli precedenti e ricordarlo per il futuro:

Int cache; int fibo (int n) (if (cache [n] \u003d\u003d 0) (if (n \u003d\u003d 1 || n \u003d\u003d 2) (cache [n] \u003d 1;) else (cache [n] \u003d fibo (n - 1) + fibo (n - 2);)) cache di ritorno [n];)

Poiché in questa attività, per calcolare l'N-esimo valore, è garantito che abbiamo bisogno di (N-1) -esimo, non sarà difficile riscrivere la formula in forma iterativa - riempiremo semplicemente il nostro array in una riga fino a raggiungere la cella desiderata:

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

Ora possiamo notare che quando calcoliamo il valore di F (N), allora il valore di F (N-3) ci è già garantito mai non sarà necessario. Cioè, abbiamo solo bisogno di memorizzare due valori in memoria: F (N-1) e F (N-2). Inoltre, non appena abbiamo calcolato F (N), la memorizzazione di F (N-2) perde ogni significato. Proviamo a scrivere questi pensieri in codice:

// Due valori precedenti: int cache1 \u003d 1; int cache2 \u003d 1; // Nuovo valore int cache3; for (int i \u003d 2; i<= n; i++) { cache3 = cache1 + cache2; //Вычисляем новое значение //Абстрактный cache4 будет равен cache3+cache2 //Значит cache1 нам уже не нужен?.. //Отлично, значит cache1 -- то значение, которое потеряет актуальность на следующей итерации. //cache5 = cache4 - cache3 => cache2 perderà la sua rilevanza dopo l'iterazione, ad es. dovrebbe diventare cache1 // In altre parole, cache1 - f (n-2), cache2 - f (n-1), cache3 - f (n). // Sia N \u003d n + 1 (il numero che calcoliamo alla successiva iterazione). Allora n-2 \u003d N-3, n-1 \u003d N-2, n \u003d N-1. // In accordo con le nuove realtà, riscriviamo i valori delle nostre variabili: cache1 \u003d cache2; cache2 \u003d cache3; ) cout<< cache3;

Un programmatore esperto capisce che il codice sopra è, in generale, senza senso, poiché cache3 non viene mai utilizzato (viene immediatamente scritto in cache2) e l'intera iterazione può essere riscritta utilizzando una sola espressione:

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

Per coloro che non riescono a capire come funziona la magia con il resto della divisione, o vogliono semplicemente vedere una formula più non ovvia, c'è un'altra soluzione:

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

Prova a seguire l'esecuzione di questo programma: sarai convinto che l'algoritmo sia corretto.

P.S. In generale, esiste un'unica formula per calcolare qualsiasi numero di Fibonacci che non richiede alcuna iterazione o ricorsione:

Const double SQRT5 \u003d sqrt (5); const double PHI \u003d (SQRT5 + 1) / 2; int fibo (int n) (return int (pow (PHI, n) / SQRT5 + 0,5);)

Ma, come puoi immaginare, il problema è che il costo del calcolo delle potenze dei non interi è piuttosto alto, così come il loro errore.

I programmatori dovrebbero stufarsi dei numeri di Fibonacci. Esempi del loro calcolo sono usati ovunque. Tutto dal fatto che questi numeri forniscono l'esempio più semplice di ricorsione. Sono anche un buon esempio di programmazione dinamica. Ma è necessario calcolarli così in un progetto reale? Non. Né la ricorsione né la programmazione dinamica sono opzioni ideali. E non una formula chiusa che utilizza numeri in virgola mobile. Ora ti dirò come farlo bene. Ma prima, esaminiamo tutte le soluzioni conosciute.

Il codice è per Python 3, sebbene dovrebbe andare anche per Python 2.

Innanzitutto, lascia che ti ricordi la definizione:

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

E F 1 \u003d F 2 \u003d 1.

Formula chiusa

Salteremo i dettagli, ma chi lo desidera può familiarizzare con la derivazione della formula. L'idea è di assumere che ci sia qualche x per cui F n \u003d x n, e quindi trovare x.

Cosa fa

Riduci x n-2

Risolviamo l'equazione quadratica:

Da dove nasce la “sezione aurea” ϕ \u003d (1 + √5) / 2. Sostituendo i valori iniziali e facendo altri calcoli, otteniamo:

Che è ciò che usiamo per calcolare F n.

From __future__ import division import math def fib (n): SQRT5 \u003d math.sqrt (5) PHI \u003d (SQRT5 + 1) / 2 return int (PHI ** n / SQRT5 + 0.5)

Buona:
Facile e veloce per piccoli n
Male:
Operazioni in virgola mobile richieste. Grande n richiederà più precisione.
Il male:
Usare numeri complessi per calcolare F n è bello da un punto di vista matematico, ma brutto da un punto di vista informatico.

Ricorsione

La soluzione più ovvia, che hai già visto molte volte, è molto probabilmente un esempio di cosa sia la ricorsione. Lo ripeterò ancora, per completezza. In Python, può essere scritto su una riga:

Fib \u003d lambda n: fib (n - 1) + fib (n - 2) se n\u003e 2 altrimenti 1

Buona:
Implementazione molto semplice che ripete la definizione matematica
Male:
Tempo di esecuzione esponenziale. Molto lento per grandi n
Il male:
Stack overflow

Memorizzazione

La soluzione ricorsiva ha un grosso problema: i calcoli intersecanti. Quando si chiama fib (n), vengono contati fib (n-1) e fib (n-2). Ma quando viene contato fib (n-1), conta di nuovo indipendentemente fib (n-2), ovvero fib (n-2) viene contato due volte. Se continuiamo il ragionamento, vedremo che fib (n-3) verrà contato tre volte, e così via. Troppi incroci.

Pertanto, devi solo memorizzare i risultati in modo da non contarli di nuovo. Il tempo e la memoria per questa soluzione vengono consumati in modo lineare. Sto usando un dizionario nella soluzione, ma potrebbe anche essere usato un semplice array.

M \u003d (0: 0, 1: 1) def fib (n): se n in M: ritorno M [n] M [n] \u003d fib (n - 1) + fib (n - 2) ritorno M [n]

(In Python, questo può essere fatto anche con il decoratore, functools.lru_cache.)

Buona:
Basta trasformare la ricorsione in una soluzione per ricordare. Converte il tempo di esecuzione esponenziale in tempo lineare, che consuma più memoria.
Male:
Spreca molta memoria
Il male:
Possibile overflow dello stack, come la ricorsione

Programmazione dinamica

Dopo aver risolto ricordando, diventa chiaro che non abbiamo bisogno di tutti i risultati precedenti, ma solo degli ultimi due. In alternativa, invece di iniziare da fib (n) e camminare all'indietro, puoi iniziare da fib (0) e camminare in avanti. Il codice seguente ha un tempo di esecuzione lineare e un utilizzo della memoria fisso. In pratica, la velocità della soluzione sarà ancora maggiore, poiché non ci sono chiamate di funzione ricorsive e lavoro associato. E il codice sembra più semplice.

Questa soluzione è spesso citata come esempio di programmazione dinamica.

Def fib (n): a \u003d 0 b \u003d 1 per __ nell'intervallo (n): a, b \u003d b, a + b restituisce a

Buona:
Funziona velocemente per piccoli n, codice semplice
Male:
Runtime ancora lineare
Il male:
Niente di speciale.

Algebra delle matrici

E infine, la soluzione meno illuminata, ma più corretta, utilizzando saggiamente tempo e memoria. Può anche essere esteso a qualsiasi sequenza lineare omogenea. L'idea è di utilizzare matrici. Basta solo vederlo

Una generalizzazione di questo lo suggerisce

I due valori di x che abbiamo ottenuto in precedenza, uno dei quali era la sezione aurea, sono gli autovalori della matrice. Pertanto, un altro modo per derivare una formula chiusa consiste nell'utilizzare un'equazione di matrice e un'algebra lineare.

Allora perché questa formulazione è utile? Il fatto che l'elevazione a potenza può essere eseguita in tempo logaritmico. Questo viene fatto tramite la quadratura. La linea di fondo è quella

Dove la prima espressione è usata per A pari, la seconda per dispari. Non resta che organizzare la moltiplicazione della matrice e il gioco è fatto. Risulta il codice seguente. Ho organizzato un'implementazione ricorsiva di pow perché è più facile da capire. Vedi la versione iterativa qui.

Def pow (x, n, I, mult): "" "Restituisce x all'ennesima potenza. Assume che I sia la matrice identità che viene moltiplicata per mult e n è un numero intero positivo" "" se n \u003d\u003d 0: return I elif n \u003d\u003d 1: return x else: y \u003d pow (x, n // 2, I, mult) y \u003d mult (y, y) if n% 2: y \u003d mult (x, y) return y def identity_matrix (n): "" "Restituisce la matrice identità n per n" "" r \u003d list (range (n)) return [for j in r] def matrix_multiply (A, B): BT \u003d list (zip (* B) ) return [for row_a in A] def fib (n): F \u003d pow ([,], n, identity_matrix (2), matrix_multiply) return F

Buona:
Memoria fissa, tempo logaritmico
Male:
Codice più complesso
Il male:
Dobbiamo lavorare con le matrici, anche se non sono poi così male

Confronto delle prestazioni

Vale solo la pena confrontare la variante della programmazione dinamica e la matrice. Se li confrontiamo per il numero di cifre nel numero n, risulta che la soluzione matriciale è lineare e la soluzione con programmazione dinamica è esponenziale. Un esempio pratico è il calcolo di fib (10 ** 6), un numero che avrà più di duecentomila caratteri.

N \u003d 10 ** 6
Calcola fib_matrix: fib (n) ha 208988 cifre in totale, il calcolo ha richiesto 0,24993 secondi.
Calcola fib_dynamic: fib (n) ha 208988 cifre in totale e ha impiegato 11,83377 secondi.

Osservazioni teoriche

Senza toccare direttamente il codice sopra, questa osservazione è ancora di un certo interesse. Considera il grafico seguente:

Contiamo il numero di cammini di lunghezza n da A a B. Ad esempio, per n \u003d 1 abbiamo un cammino, 1. Per n \u003d 2 abbiamo ancora un cammino, 01. Per n \u003d 3 abbiamo due cammini, 001 e 101 Si può mostrare molto semplicemente che il numero di cammini di lunghezza n da A a B è esattamente uguale a F n. Scrivendo la matrice di adiacenza per il grafico, otteniamo la stessa matrice descritta sopra. Questo è un risultato ben noto della teoria dei grafi che per una data matrice di adiacenza A, le occorrenze in A n sono il numero di cammini di lunghezza n nel grafo (uno dei problemi menzionati nel film "Good Will Hunting").

Perché ci sono tali simboli sui bordi? Si scopre che quando si guarda una sequenza infinita di caratteri su una sequenza infinita di percorsi in entrambe le direzioni su un grafico, si ottiene qualcosa chiamato "subshifts di tipo finito", che è un tipo di sistema dinamico simbolico. Questo particolare subshift del tipo finito è noto come "spostamento della sezione aurea" ed è specificato da un insieme di "parole proibite" (11). In altre parole, otteniamo sequenze binarie infinite in entrambe le direzioni e nessuna coppia di esse sarà adiacente. L'entropia topologica di questo sistema dinamico è uguale alla sezione aurea ϕ. È interessante come questo numero appaia periodicamente in diverse aree della matematica.

Numeri di Fibonacci È una serie di numeri in cui ogni numero successivo è uguale alla somma dei due precedenti: 1, 1, 2, 3, 5, 8, 13, .... A volte la riga inizia da zero: 0, 1, 1, 2, 3, 5, .... In questo caso, ci atterremo alla prima opzione.

Formula:

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

Esempio di calcolo:

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
FA 6 \u003d FA 5 + FA 4 \u003d 5 + 3 \u003d 8
...

Calcolo dell'ennesimo numero di una serie di Fibonacci utilizzando un ciclo while

  1. Assegna alle variabili fib1 e fib2 i valori dei primi due elementi della riga, ovvero assegna unità alle variabili.
  2. Chiedere all'utente il numero dell'elemento, il valore di cui vuole ottenere. Assegna un numero alla variabile n.
  3. Eseguire i seguenti passaggi n - 2 volte, poiché i primi due elementi sono già stati presi in considerazione:
    1. Aggiungi fib1 e fib2, assegnando il risultato a una variabile di archiviazione temporanea, come fib_sum.
    2. Imposta la variabile fib1 su fib2.
    3. Imposta la variabile fib2 su fib_sum.
  4. Visualizza il valore fib2.

Nota. Se l'utente inserisce 1 o 2, il corpo del ciclo non viene mai eseguito, viene visualizzato il valore originale di fib2.

fib1 \u003d 1 fib2 \u003d 1 n \u003d input () n \u003d int (n) i \u003d 0 mentre i< n - 2 : fib_sum = fib1 + fib2 fib1 = fib2 fib2 = fib_sum i = i + 1 print (fib2)

Versione compatta del codice:

fib1 \u003d fib2 \u003d 1 n \u003d int (input ( "Numero dell'elemento della serie di Fibonacci:")) - 2 mentre n\u003e 0: fib1, fib2 \u003d fib2, fib1 + fib2 n - \u003d 1 print (fib2)

Visualizzazione dei numeri di Fibonacci con un ciclo for

In questo caso, non viene visualizzato solo il valore dell'elemento desiderato della serie di Fibonacci, ma anche tutti i numeri fino a includerlo. Per fare ciò, l'output del valore fib2 viene inserito in un ciclo.

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

Esempio di esecuzione:

10 1 1 2 3 5 8 13 21 34 55

Calcolo ricorsivo dell'ennesimo numero della serie di Fibonacci

  1. Se n \u003d 1 o n \u003d 2, restituisci uno al ramo chiamante, poiché il primo e il secondo elemento della serie di Fibonacci sono uguali a uno.
  2. In tutti gli altri casi, chiamare la stessa funzione con gli argomenti n - 1 e n - 2. Aggiungere il risultato delle due chiamate e restituirlo al ramo del programma chiamante.

def fibonacci (n): if n in (1, 2): return 1 return fibonacci (n - 1) + fibonacci (n - 2) print (fibonacci (10))

Diciamo n \u003d 4. Quindi fibonacci (3) e fibonacci (2) sono chiamati ricorsivamente. Il secondo restituirà uno e il primo darà come risultato altre due chiamate di funzione: fibonacci (2) e fibonacci (1). Entrambe le chiamate ne restituiranno una, per un totale di due. Quindi la chiamata a fibonacci (3) restituisce il numero 2, che viene aggiunto al numero 1 della chiamata a fibonacci (2). Il risultato 3 viene restituito al mainstream. Il quarto elemento della serie di Fibonacci è uguale a tre: 1 1 2 3.

LA CAMPANA

C'è chi ha letto questa notizia prima di te.
Iscriviti per ricevere gli ultimi articoli.
E-mail
Nome
Cognome
Come vuoi leggere The Bell
Niente spam