Algoritmi ricorsivi
In questa pagina 5
Un algoritmo ricorsivo invoca se stesso su istanze sempre più piccole. La soluzione di un'istanza di taglia si ottiene:
- direttamente per i casi base ;
- altrimenti usando le soluzioni di una o più istanze di taglia .
È l'induzione trasformata in programma: per questo la correttezza si prova per induzione (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 →).
Esempi
Algoritmo ReverseArray(A, i, j) Algoritmo LinearSum(A, n)
Input: array A, indici i, j >= 0 Input: array A, intero n >= 1
Output: A[i..j] ribaltato in loco Output: somma dei primi n elementi
if i < j then if n = 1 then return A[0]
swap(A[i], A[j]) else return LinearSum(A, n-1) + A[n-1]
ReverseArray(A, i+1, j-1)
returnAlbero della ricorsione e stack
L'esecuzione su una data istanza è descritta dall'albero della ricorsione: ogni nodo è una chiamata, la radice è la prima, i figli di un nodo sono le chiamate fatte direttamente da esso, le foglie sono i casi base.
A runtime, la memoria si divide in stack (variabili locali e riferimenti: ogni chiamata aggiunge un record di attivazione e lo toglie quando finisce, con politica LIFO, e si accede solo all'ultimo inserito) e heap (oggetti, o strutture allocate con malloc). In LinearSum(A, 5) la chiamata LinearSum(A, 3) crea il suo record sopra quelli di n=5 e n=4; una ricorsione di profondità occupa di stack.
Complessità con l'albero della ricorsione
A ogni nodo si attribuisce il costo delle operazioni della sua chiamata escluse le chiamate ricorsive; il costo totale è la somma sui nodi. Per un limite superiore si stima per ogni istanza di taglia , per un limite inferiore si esibisce un'istanza (o una stima valida per tutte).
- LinearSum: nodi (una chiamata per ogni ), ciascuno : .
- ReverseArray, con taglia : ogni chiamata riduce la taglia di , quindi chiamate di costo costante: .
Potenze in tempo logaritmico
se ; se dispari; se pari e positivo.
Algoritmo Power(x, n)
Input: x reale, n >= 0 intero
Output: x^n
if n = 0 then return 1
if n è dispari then
y <- Power(x, (n-1)/2)
return x * y * y
else
y <- Power(x, n/2)
return y * yAlla -esima chiamata l'esponente è : l'albero è un cammino di nodi, ciascuno . . Contando le chiamate: ne richiede (), contro le moltiplicazioni dell'approccio ingenuo. Si calcola y una volta sola e si riusa: chiamare Power due volte riporterebbe a .
Con la formula (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 →), PowerFib(n) costa operazioni aritmetiche su reali (in pratica, con numeri a precisione finita, per grandi serve aritmetica esatta).
Fibonacci ricorsivo e memoizzazione
fib(n): se n <= 1 restituisci n; altrimenti restituisci fib(n-1) + fib(n-2)Il numero di chiamate soddisfa e , cioè : (; per sono chiamate, verificato eseguendo il codice). Cresce come : esponenziale. Lo spreco è nei sottoproblemi ripetuti ( è ricalcolato più volte). Con la memoizzazione (si salva ogni risultato in una tabella e lo si riusa) per le chiamate scendono a e in generale a (ciascun è calcolato una volta sola).
Correttezza per induzione
Sia la taglia. Si dimostra la correttezza per i casi base ; poi, assumendo che l'algoritmo risolva correttamente tutte le istanze di taglia , si dimostra che risolve quelle di taglia .
LinearSum. Base : restituisce ✓. Passo: per ipotesi LinearSum(A, n) , quindi LinearSum(A, n+1) ✓.
Power. Base : ✓. Passo: se è pari, per ipotesi e ; se è dispari, e ✓.
Errori comuni
- Dimenticare un caso base, o una chiamata che non riduce la taglia (non termina): in
ReverseArrayserve . - Contare il costo di un nodo includendo le chiamate figlie (le conta due volte).
- Ricalcolare più volte la stessa sottoistanza (
Powercon due chiamate,fibsenza memoizzazione). - Dimenticare che la ricorsione consuma stack: profondità ⇒ spazio .
Versione ripasso
- Ricorsione = induzione: casi base risolti direttamente; altrimenti chiamate su istanze più piccole. La correttezza si prova per induzione (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 →).
- Albero della ricorsione: nodo = chiamata; costo del nodo = operazioni escluse le chiamate figlie; costo totale = somma sui nodi. Stack: un record di attivazione per chiamata (LIFO), profondità ⇒ spazio .
- LinearSum ; ReverseArray (taglia ) .
- Power(, ): , (dispari), (pari); una sola chiamata ricorsiva, chiamate, ; PowerFib .
- Fibonacci ricorsivo: chiamate (: ), esponenziale per sottoproblemi ripetuti; con memoizzazione (: chiamate).
- Correttezza: base + ipotesi su tutte le taglie ⇒ taglia .
- Esempi:
ReverseArray(A,i,j): se ,swap(A[i], A[j])e ricorsione su ;LinearSum(A,n): , altrimentiLinearSum(A,n-1) + A[n-1]. - Numeri di chiamate: : per ;
Power(1000): chiamate. - Errori: caso base mancante o taglia che non scende; contare le chiamate figlie nel costo del nodo; due chiamate dove ne basta una.