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 a destra del segno uguale è 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 → e RicorsioneFunzioni che chiamano se stesse: caso base e passo ricorsivo, stack delle chiamate, esempi su numeri, stringhe e liste, ricorsione multipla e suo costo, divide et impera, confronto con l'iterazione.Ricorsione →):
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à (lo spazio è quindi anche se il tempo è esponenziale: i record vengono tolti e riusati; il funzionamento dello stack è in Stack e chiamata di funzioni in ARMChiamata con BL e ritorno con BX LR; convenzione di chiamata AAPCS (argomenti in r0-r3, risultato in r0, registri da preservare r4-r11); stack discendente pieno con PUSH e POP; record di attivazione; funzioni foglia e non foglia; esempio ricorsivo del fattoriale con evoluzione dello stack.Stack e chiamata di funzioni in ARM →).
Stack delle chiamate nei primi passi (cima a destra; ogni record ha il suo parametro n e, dopo la prima chiamata figlia, il valore parziale ricevuto):
| passo | stack (dal fondo alla cima) | evento |
|---|---|---|
| 1 | prima chiamata | |
| 2 | chiama | |
| 3 | ||
| 4 | ||
| 5 | caso base: restituisce e il record viene tolto | |
| 6 | chiama la seconda figlia: caso base, restituisce | |
| 7 | ha ricevuto e : restituisce e viene tolto | |
| 8 | chiama e riceve : | |
| 9 | ha ricevuto e chiama , e così via |
(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 : base , ; passo (Principio di induzionePer dimostrare una proprietà per ogni n basta il passo base e il passo induttivo da n a n+1.Principio di induzione →, 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 →).
Grafico interattivo: Numero di chiamate di fibonacci(n), C(n) = 2F(n+1) − 1 ≈ 2·Φ^(n+1)/√5 − 1 (15 per n = 5, 1973 per n = 15), a confronto con le 2n − 1 della versione con memoizzazione (9 per n = 5): la crescita è esponenziale contro lineare
(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. Per induzione a passo : base ; se , allora . Controllo: .
- Limite superiore: dà : aggiungendo a entrambi i membri, , quindi . Controllo: .
- 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 : la discesa fa chiamate e riempie , , , ; le altre (la seconda figlia di , di e di ) trovano subito il valore in o sono un caso base.
- 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. Algoritmo fibonacci(n): se restituisce , altrimenti . Per : (a) esecuzione passo per passo, con albero o stack delle chiamate; (b) numero di chiamate; (c) complessità al caso pessimo; (d) come migliorarlo.
- (a) Albero delle chiamate (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 →, RicorsioneFunzioni che chiamano se stesse: caso base e passo ricorsivo, stack delle chiamate, esempi su numeri, stringhe e liste, ricorsione multipla e suo costo, divide et impera, confronto con l'iterazione.Ricorsione →): , con figli e . La chiamata sinistra si risolve per intero prima della destra. Lo stack più profondo ha record: .
- Stack nei primi passi (cima a destra): passo 1 ; passo 2 ; passo 3 ; passo 4 ; passo 5 caso base, restituisce .
- (b) Nodi per valore: : ; : ; : ; : ; : ; : . Totale 15 chiamate, cioè ricorsive oltre alla prima. Formula generale: (Principio di induzionePer dimostrare una proprietà per ogni n basta il passo base e il passo induttivo da n a n+1.Principio di induzione →, 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 →).
- (c) Ogni nodo costa , quindi , con . Limiti: . Esponenziale.
- (d) Sottoproblemi ripetuti: due volte, tre, cinque. Memoizzazione: tempo e spazio, per le chiamate scendono da a . Iterativo dal basso: tempo, spazio. Potenza veloce: .
- Iterativo:
a <- 0; b <- 1; for i <- 2 to n: (a, b) <- (b, a + b); return b. - Errori tipici: dire che l'albero ha nodi (ne ha ); contare male; concludere perché ci sono livelli; proporre un ciclo senza dire che il guadagno sta nel non ripetere sottoproblemi.