Salta al contenuto
Note per Studenti Esercizio 5 · Fibonacci ricorsivo, chiamate e complessità

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) = 1

Ordine di esecuzione (la chiamata sinistra si risolve per intero prima della destra): f(5)→f(4)→f(3)→f(2)→f(1)=1f(5) \to f(4) \to f(3) \to f(2) \to f(1) = 1, poi f(0)=0f(0) = 0 e f(2)=1+0=1f(2) = 1 + 0 = 1; poi f(1)=1f(1) = 1 e f(3)=1+1=2f(3) = 1 + 1 = 2; poi f(2)f(2) destra di f(4)f(4): f(1)=1f(1) = 1, f(0)=0f(0) = 0, f(2)=1f(2) = 1, quindi f(4)=2+1=3f(4) = 2 + 1 = 3; poi f(3)f(3) destra di f(5)f(5) (che ripete tutto il lavoro svolto per f(3)f(3) a sinistra): f(3)=2f(3) = 2 e infine f(5)=3+2=5f(5) = 3 + 2 = 5. Nello stack, al momento più profondo, ci sono 55 record: f(5),f(4),f(3),f(2),f(1)f(5), f(4), f(3), f(2), f(1), cioè profondità nn.

(b) Numero di chiamate

Contando i nodi dell'albero: f(5)f(5): 11; f(4)f(4): 11; f(3)f(3): 22; f(2)f(2): 33; f(1)f(1): 55; f(0)f(0): 33. Totale 1+1+2+3+5+3=151 + 1 + 2 + 3 + 5 + 3 = \mathbf{15} chiamate (compresa la prima), cioè 1414 chiamate ricorsive oltre alla chiamata iniziale. (Dipende dalla lettura del testo: "chiamate effettuate" va dichiarato; qui 1515 nodi dell'albero, verificato eseguendo il codice.)

Contare le chiamate in generale: C(0)=C(1)=1C(0) = C(1) = 1 e C(n)=1+C(n−1)+C(n−2)C(n) = 1 + C(n-1) + C(n-2), cioè 1,1,3,5,9,15,25,41,67,…1, 1, 3, 5, 9, 15, 25, 41, 67, \dots. Si dimostra per induzione che C(n)=2F(n+1)−1C(n) = 2F(n+1) - 1.

(c) Complessità al caso pessimo

Il costo di ogni nodo (escluse le chiamate figlie) è Θ(1)\Theta(1), quindi il tempo è proporzionale al numero di nodi: T(n)∈Θ(C(n))=Θ(F(n+1))T(n) \in \Theta(C(n)) = \Theta(F(n+1)).

Quindi T(n)∈Θ(Φn)T(n) \in \Theta(\Phi^n), in particolare 2n/2≲T(n)≲2n2^{n/2} \lesssim T(n) \lesssim 2^n: esponenziale, inefficiente anche per n≈50n \approx 50.

(d) Come migliorarlo

Lo spreco sono i sottoproblemi ripetuti: f(3)f(3) è calcolato 2 volte, f(2)f(2) 3 volte, f(1)f(1) 5 volte. Rimedi:

  1. Memoizzazione: una tabella M[0..n]M[0..n]; prima di calcolare f(k)f(k) si controlla M[k]M[k], altrimenti si calcola e si salva. Ogni f(k)f(k) è calcolato una volta: Θ(n)\Theta(n) tempo e spazio. Per f(5)f(5) le chiamate scendono da 1515 a 99.
  2. Iterativo (dal basso): a <- 0; b <- 1; for i <- 2 to n: (a, b) <- (b, a+b); return b (con n≥1n \ge 1): Θ(n)\Theta(n) tempo, Θ(1)\Theta(1) spazio.
  3. Potenza veloce: con la formula F(n)=15(Φn−Φ^n)F(n) = \frac{1}{\sqrt5}(\Phi^n - \hat\Phi^n) e l'algoritmo Power (o con potenze di matrici 2×22 \times 2 a interi, che evitano gli errori di arrotondamento): O(log⁡n)O(\log n) 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 nn nodi (è il numero di livelli): ha Θ(Φn)\Theta(\Phi^n) nodi.
  • Contare male il numero di chiamate: per f(5)f(5) sono 1515 nodi (non 55, non 88).
  • Concludere Θ(n)\Theta(n) perché "ci sono nn 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. fibonacci(n)\text{fibonacci}(n): se n≤1n \le 1 restituisce nn, altrimenti fib(n−1)+fib(n−2)\text{fib}(n-1) + \text{fib}(n-2). (a) esecuzione di fib(5)\text{fib}(5) (albero o stack); (b) numero di chiamate; (c) complessità al caso pessimo; (d) come migliorare.

Teoria collegata