Algoritmi ricorsivi
In questa pagina 3
Definizione (algoritmo ricorsivo). 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 .
Esempio. LinearSum(A, n) ha il caso base e per usa LinearSum(A, n-1): su con restituisce .
È l'induzione trasformata in programma: il caso base della ricorsione è il caso base dell'induzione, e la chiamata su un'istanza più piccola corrisponde all'ipotesi induttiva. 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 → e 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 →). La ricorsione come tecnica di programmazione è introdotta in 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 → (Fondamenti di Informatica).
Traccia di LinearSum(A, 3) con : la chiamata con aspetta LinearSum(A,2), che aspetta LinearSum(A,1) ; risalendo, LinearSum(A,2) e LinearSum(A,3) . Il risultato si costruisce al ritorno dalle chiamate.
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). La divisione è spiegata in Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →; come il processore costruisce e distrugge i record di attivazione è 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 →. 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 (si passa da a ), quindi chiamate di costo costante: . Traccia su , :
(0,4)scambia e dà ;(1,3)dà ;(2,2)ha falso e termina. Sono chiamate. Per ((0,3),(1,2),(2,1)) sono ancora : l'ultima chiamata ha taglia o ed è il caso base.
Potenze in tempo logaritmico
Formula (potenza in tempo logaritmico). se ; se dispari; se pari e positivo.
Esempio. Power(2, 10) calcola con chiamate () e restituisce .
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 (la prima è la -esima) l'esponente vale : se è pari si passa a , se è dispari a , in entrambi i casi la parte intera della metà. La ricorsione si ferma quando l'esponente è , cioè quando (stesso ragionamento della ricerca binaria, vedi Problemi computazionali e algoritmiProblema computazionale come insieme di coppie (istanza, soluzione); algoritmo e modello di calcolo RAM; pseudocodice; taglia di un'istanza; ADT e struttura dati concreta; esempio svolto con ricerca lineare e binaria in un array ordinato.Problemi computazionali e algoritmi → e Esponenziale e logaritmoLa funzione esponenziale a^x (base positiva diversa da 1) e la sua inversa, il logaritmo in base a, con grafici e proprietà.Esponenziale e logaritmo →), per . L'albero è un cammino di nodi, ciascuno . . Contando le chiamate: ne richiede (), contro le moltiplicazioni dell'approccio ingenuo.
Traccia di Power(2, 10): le chiamate scendono con esponenti e i valori risalgono così: Power(2,0) ; Power(2,1) ( dispari, ) ; Power(2,2) ( pari, ) ; Power(2,5) ( dispari, ) ; Power(2,10) () . 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 →; è la sezione aurea), 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)Formula (numero di chiamate di fib). 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).
Perché . Per induzione (due casi base, perché si usano due valori precedenti, 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 →). Base: e . Passo: . Poiché (il secondo termine tende a ), : ogni incremento di moltiplica le chiamate per . Chiamate di , :
| valore chiamato | |||||
|---|---|---|---|---|---|
| numero di chiamate | 1 | 1 | 2 | 3 | 2 |
Somma ; il sottoproblema è calcolato due volte e tre. Il grafico confronta le chiamate senza memoizzazione (curva , che coincide con ai valori interi con errore inferiore a ) con quelle con memoizzazione, :
{"tipo":"funzione","titolo":"Chiamate di fib(n): senza memoizzazione crescono come 2Φ^(n+1)/√5 − 1 (C(5) = 15, C(10) = 177), con memoizzazione sono 2n − 1 (9 per n = 5, 19 per n = 10)","curve":[{"espressione":"2*((1+sqrt(5))/2)^(x+1)/sqrt(5)-1","etichetta":"senza memoizzazione: C(n)"},{"espressione":"2*x-1","etichetta":"con memoizzazione: 2n−1"}],"x":[1,12],"y":[0,500],"punti":[{"x":5,"y":15,"etichetta":"15"},{"x":10,"y":177,"etichetta":"177"}],"assi":{"x":"n","y":"chiamate"}}
``` 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). Per : la discesa fa chiamate e riempie la tabella; poi chiama (caso base, una chiamata), chiama (già in tabella) e chiama (già in tabella): . In generale chiamate di discesa più chiamate che trovano già il risultato, circa . La memoizzazione usa di memoria per la tabella: è lo scambio tempo contro spazio.
*Esempio.* chiamate per `fib(5)`.
## 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: si assume che `Power` sia corretta per **tutti** gli esponenti da a (ipotesi forte: serve perché la chiamata ricorsiva è su circa la metà dell'esponente, non su ). Se è pari, la chiamata è su (vero per ), quindi per ipotesi e . Se è dispari, la chiamata è su , quindi e ✓ (si usa , vedi <a class="wiki glossa" href="/analisi-1/appunti/funzioni-potenza/" aria-describedby="sp-10">Funzioni potenza<span class="spiega" role="tooltip"><span class="spiega-testo" id="sp-10">Le potenze x^alfa al variare dell'esponente: domini, grafici, radici n-esime e proprietà delle potenze con base positiva.<span class="spiega-vai">Funzioni potenza →</span></span><span class="graffa" aria-hidden="true"><i class="g-l"></i><i class="g-linea"></i><i class="g-punta"></i><i class="g-linea"></i><i class="g-r"></i></span></span><!--/spiega--></a>).
## Errori comuni
- Dimenticare un caso base, o una chiamata che non riduce la taglia (non termina): in `ReverseArray` serve .
- Contare il costo di un nodo **includendo** le chiamate figlie (le conta due volte).
- Ricalcolare più volte la stessa sottoistanza (`Power` con due chiamate, `fib` senza memoizzazione).
- Dimenticare che la ricorsione consuma stack: profondità ⇒ spazio .
Versione ripasso
- Definizione: un algoritmo ricorsivo risolve i casi base direttamente e le altre istanze usando istanze di taglia minore. 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 →, 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 →).
- Albero della ricorsione: ogni nodo è una chiamata; il costo del nodo esclude le chiamate figlie; il costo totale è la somma sui nodi.
- Stack: ogni chiamata aggiunge un record di attivazione, tolto alla fine (LIFO); profondità , spazio (vedi Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →).
- LinearSum(A, n): dà , altrimenti
LinearSum(A, n-1) + A[n-1]; . Su dà . - ReverseArray(A, i, j): se scambia e , poi ricorre su ; con taglia fa chiamate, . Su sono chiamate.
- Power(x, n): ; dispari: con ; pari: con . Una chiamata ricorsiva per livello, chiamate, .
Power(2,10)con esponenti .yva calcolato una volta sola: chiamare due volte riporta a . - PowerFib: con
Power, operazioni su reali. - Fibonacci ricorsivo: , : per . Cresce come , esponenziale. Per sono chiamate, con due volte e tre.
- Memoizzazione: ogni è calcolato una volta: tempo e memoria. Per sono chiamate: di discesa, più che trovano il valore o sono casi base.
- Correttezza: base, poi ipotesi su tutte le taglie per dimostrare la taglia . Per
LinearSumsi usa come ipotesi; perPowerserve l'ipotesi forte su tutti gli esponenti , perché la chiamata è su circa . - Traccia ReverseArray su :
(0,4)scambia e dando ;(1,3)dà ;(2,2)ha falso e termina. - Costo di Power: con servono chiamate, contro le moltiplicazioni dell'approccio ingenuo; l'albero della ricorsione è un cammino.
- Errori tipici: caso base mancante o taglia che non scende (in ReverseArray serve ); contare nel costo del nodo le chiamate figlie; ricalcolare la stessa sottoistanza; dimenticare lo stack.
Esercizi su questo argomento
- Esercizio - funzione g(n) definita per ricorrenza, calcolo memoizzato e formula chiusa (compito del 19 giugno 2024, es. 2)
- Esercizio 5 · Fibonacci ricorsivo, chiamate e complessità
- Esercizio 9 · foglia più profonda di un albero binario proprio
- Esercizio 11 · massimo beneficio lungo un cammino radice-foglia
- Esercizio 24 · C, lanterne estive (lista concatenata)
- Esercizio 28 · C, palindromi in una lista
Teoria collegata
- Divide and conquer - proprietà di sottostruttura e albero delle chiamate
- Paradigma greedy - scelta golosa, sottostruttura e selezione delle attività
- Polinomi - rappresentazione per coefficienti e per valori e convoluzione
- Alberi
- Alberi binari di ricerca
- Alberi e heap in C
- Algoritmi di ordinamento
- Grafi diretti e ordinamento topologico
- Liste concatenate in C
- Liste, pile e code
- Puntatori, struct e memoria dinamica in C
- Visite di alberi
- Visite di grafi - BFS e DFS
- Taglio minimo randomizzato - algoritmo di Karger