Esercizio 5Fibonacci ricorsivo, chiamate e complessità
In questa pagina 5
Testo (scritto del 09/07/2024, seconda parte, esercizio 1, 4 punti). Si consideri il seguente algoritmo ricorsivo.
Algoritmo fibonacci(n)
Input: numero intero n
Output: valore n-esimo della sequenza di Fibonacci
if n <= 1 then return n
else return fibonacci(n-1) + fibonacci(n-2)(a) Descrivere passo dopo passo l'esecuzione per fibonacci(5), mostrando i valori di tutte le variabili in ogni passo (usare l'albero delle chiamate oppure lo stack delle chiamate).
(b) Calcolare il numero totale di chiamate ricorsive effettuate per fibonacci(5).
(c) Analizzare la complessità temporale al caso pessimo dell'algoritmo.
(d) Descrivere un'idea per modificare l'algoritmo allo scopo di migliorarne l'efficienza nel tempo.
(a) Albero delle chiamate
Ogni nodo è una chiamata, il valore tra parentesi è ciò che restituisce (vedi Algoritmi ricorsiviAlgoritmi ricorsivi come induzione eseguita; albero della ricorsione e record di attivazione nello stack; esempi ReverseArray, LinearSum, Power in tempo logaritmico, Fibonacci ricorsivo (esponenziale) e con memoizzazione; analisi della complessità con l'albero della ricorsione e correttezza per induzione.Algoritmi ricorsivi →):
f(5) = 5
├─ f(4) = 3
│ ├─ f(3) = 2
│ │ ├─ f(2) = 1
│ │ │ ├─ f(1) = 1
│ │ │ └─ f(0) = 0
│ │ └─ f(1) = 1
│ └─ f(2) = 1
│ ├─ f(1) = 1
│ └─ f(0) = 0
└─ f(3) = 2
├─ f(2) = 1
│ ├─ f(1) = 1
│ └─ f(0) = 0
└─ f(1) = 1Ordine di esecuzione (la chiamata sinistra si risolve per intero prima della destra): , poi e ; poi e ; poi destra di : , , , quindi ; poi destra di (che ripete tutto il lavoro svolto per a sinistra): e infine . Nello stack, al momento più profondo, ci sono record: , cioè profondità .
(b) Numero di chiamate
Contando i nodi dell'albero: : ; : ; : ; : ; : ; : . Totale chiamate (compresa la prima), cioè chiamate ricorsive oltre alla chiamata iniziale. (Dipende dalla lettura del testo: "chiamate effettuate" va dichiarato; qui nodi dell'albero, verificato eseguendo il codice.)
Contare le chiamate in generale: e , cioè . Si dimostra per induzione che .
(c) Complessità al caso pessimo
Il costo di ogni nodo (escluse le chiamate figlie) è , quindi il tempo è proporzionale al numero di nodi: .
- Limite inferiore: (perché ), quindi : esponenziale.
- Limite superiore: dà .
- Valore esatto: con (formula chiusa, vedi Dimostrazioni, induzione e invariantiTecniche di dimostrazione (esempio, controesempio, assurdo), induzione con casi base multipli, invarianti di ciclo (inizializzazione, conservazione, uso alla fine), schema generale per provare la correttezza; esempi svolti su arrayMax, sequenza di bit e numeri di Fibonacci.Dimostrazioni, induzione e invarianti →).
Quindi , in particolare : esponenziale, inefficiente anche per .
(d) Come migliorarlo
Lo spreco sono i sottoproblemi ripetuti: è calcolato 2 volte, 3 volte, 5 volte. Rimedi:
- Memoizzazione: una tabella ; prima di calcolare si controlla , altrimenti si calcola e si salva. Ogni è calcolato una volta: tempo e spazio. Per le chiamate scendono da a .
- Iterativo (dal basso):
a <- 0; b <- 1; for i <- 2 to n: (a, b) <- (b, a+b); return b(con ): tempo, spazio. - Potenza veloce: con la formula e l'algoritmo
Power(o con potenze di matrici a interi, che evitano gli errori di arrotondamento): operazioni (vedi Algoritmi ricorsiviAlgoritmi ricorsivi come induzione eseguita; albero della ricorsione e record di attivazione nello stack; esempi ReverseArray, LinearSum, Power in tempo logaritmico, Fibonacci ricorsivo (esponenziale) e con memoizzazione; analisi della complessità con l'albero della ricorsione e correttezza per induzione.Algoritmi ricorsivi →).
Errori comuni
- Dire che l'albero ha nodi (è il numero di livelli): ha nodi.
- Contare male il numero di chiamate: per sono nodi (non , non ).
- Concludere perché "ci sono livelli": a ogni livello il lavoro raddoppia circa.
- Proporre di "usare un ciclo" senza spiegare che il guadagno viene dal non ripetere sottoproblemi.
Versione ripasso
Testo. : se restituisce , altrimenti . (a) esecuzione di (albero o stack); (b) numero di chiamate; (c) complessità al caso pessimo; (d) come migliorare.
- (a) albero (vedi Algoritmi ricorsiviAlgoritmi ricorsivi come induzione eseguita; albero della ricorsione e record di attivazione nello stack; esempi ReverseArray, LinearSum, Power in tempo logaritmico, Fibonacci ricorsivo (esponenziale) e con memoizzazione; analisi della complessità con l'albero della ricorsione e correttezza per induzione.Algoritmi ricorsivi →): ; ; ; ; risultati , , , . Stack massimo: record.
- (b) chiamate per livello di valore: , , , , , : totale 15 nodi. .
- (c) nodo ⇒ ; : esponenziale.
- (d) sottoproblemi ripetuti ( ×2, ×3, ×5): memoizzazione (9 chiamate per ); iterativo , spazio ; potenza veloce .
- Iterativo:
a <- 0; b <- 1; for i <- 2 to n: (a, b) <- (b, a + b); return b. - Errori: albero con nodi; conteggio sbagliato (15); per "n livelli".