07 2 ricorsione
- 2. Definizioni induttive
• Sono comuni in matematica nella definizione
di proprietà di sequenze numerabili
• Esempio: numeri pari
– 0 è un numero pari
– se n è un numero pari anche n+2 è un numero
pari
• Esempio: il fattoriale di un naturale N (N!)
– se N=0 il fattoriale N! è 1
– se N>0 il fattoriale N! è N * (N-1)!
- 3. Dimostrazioni per induzione
• Dimostriamo che (2 x n)2 = 4 x n2
(distributività del quadrato rispetto alla moltiplicazione)
1) se n=1 : vero (per verifica diretta)
2) suppongo sia vero per n'=k (ip. di induz.) e
lo dimostro per n=k+1:
(2n)2 = (2(k+1))2 = (2k+2)2 = (2k)2 + 8k + 4 =
(per ipotesi di induzione) 4k2 + 8k + 4 =
4(k2 + 2k + 1) = 4(k+1)2 = 4n2
1) è il caso base, 2) è il passo induttivo
- 4. Iterazione e ricorsione
• Sono i due concetti informatici che nascono dal
concetto di induzione: applicare un'azione un
insieme numerabile e finito di volte
• L'iterazione si realizza mediante la tecnica del
ciclo
• Per il calcolo del fattoriale:
– 0! = 1
– n! = n (n - 1)(n - 2)…. 1
– realizzo un ciclo che parte dal dato richiesto e applica
il passo di induzione fino a raggiungere il caso base
- 6. Progettare con la ricorsione
• Esiste un CASO BASE, che rappresenta un sotto-problema
facilmente risolvibile
• Esempio: se N=0, so N! in modo "immediato" (vale 1)
• Esiste un PASSO INDUTTIVO che ci riconduce (prima o
poi) al caso base
• L'algoritmo ricorsivo esprime la soluzione al
problema (su dati di una "dimensione" generica)
in termini di operazioni semplici e della soluzione
allo stesso problema su dati "più piccoli" (che, su
dati sufficientemente elementari, si suppone
risolto per ipotesi)
• Esempio: per N generico esprimo N! in termini di N (che è un dato
direttamente accessibile) moltiplicato per (è una operazione
semplice) il valore di (N-1)! (che so calcolare per ipotesi induttiva)
- 7. Progettare con la ricorsione
• E' un nuovo esempio del procedimento divideet-impera che spezza un problema in sottoproblemi
• Con le funzioni non ricorsive abbiamo
spezzato un problema in tanti sotto-problemi
diversi più semplici
• Con la ricorsione spezziamo il problema in
tanti sotto-problemi identici applicati a dati
più semplici
- 8. Fattoriale con la ricorsione
• 1) n! = 1 se n = 0
• 2) n! = n * (n - 1)! se n > 0
– riduce il calcolo a un calcolo più semplice
– ha senso perché si basa sempre sul fattoriale del
numero più piccolo, che io conosco
– ha senso perché si arriva a un punto in cui non è
più necessario riusare la definizione 2) e invece si
usa la 1)
– 1) è il caso base, 2) è il passo induttivo (ricorsivo)
- 9. Ricorsione nei sottoprogrammi
• Dal latino re-currere
– ricorrere, fare ripetutamente la stessa azione
• Un sottoprogramma P invoca se stesso
– Direttamente
P
• P invoca P
– oppure
– Indirettamente
• P invoca Q che invoca P
P
Q
- 11. Simulazione del calcolo di FattRic(3)
3 = 0? No calcola fattoriale di 2 e moltiplica per 3
2 = 0? No calcola fattoriale di 1 e moltiplica per 2
1 = 0? No calcola fattoriale di 0 e moltiplica per 1
0 = 0? Si fattoriale di 0 è 1
fattoriale di 1 è 1 per fattoriale di 0, cioè 1 1 = 1
fattoriale di 2 è 2 per fattoriale di 1, cioè 2 1 = 2
fattoriale di 3 è 3 per fattoriale di 2, cioè 3 2 = 6
- 12. Esecuzione di funzioni ricorsive
• In un certo istante possono essere in corso
diverse attivazioni dello stesso
sottoprogramma
– Ovviamente sono tutte sospese tranne una,
l'ultima invocata, all'interno della quale si sta
svolgendo il flusso di esecuzione
• Ogni attivazione esegue lo stesso codice ma
opera su copie distinte dei parametri e delle
variabili locali
- 13. Il modello a runtime:
esempio
int fattorialeRic(int n) {
if (n == 0)
return 1;
else {
int temp = n * fattorialeRic(n - 1);
return temp;
}
}
int main() {
int numero;
cin >> numero;
int ris = fattorialeRic(numero);
cout << "Fattoriale ricorsivo: "
<< ris << endl;
return 0;
}
assumiamo val = 3
13
val = 3
ris =
n=3
temp =
n=2
temp =
n=1
temp =
n=0
temp =
6
3* 2
2* 1
1* 1
?
temp: cella temporanea per
memorizzare il risultato della
funzione chiamata
- 14. Terminazione della ricorsione
• … se ogni volta la funzione richiama se stessa…
perché la catena di invocazioni non continua
all'infinito?
• Quando si può dire che una ricorsione è ben
definita?
• Informalmente:
– Se per ogni applicazione del passo induttivo ci si
avvicina alla situazione riconosciuta come caso base,
allora la definizione è ben formata e la catena di
invocazioni termina
- 15. Un altro esempio: la serie di Fibonacci
• Fibonacci (1202) partì dallo studio sullo
sviluppo di una colonia di conigli in circostanze
ideali
• Partiamo da una coppia di conigli
• I conigli possono riprodursi all'età di un mese
• Supponiamo che dal secondo mese di vita in poi, ogni
femmina produca una nuova coppia
• e inoltre che i conigli non muoiano mai…
– Quante coppie ci sono dopo n mesi?
- 16. Definizione ricorsiva della serie
• I numeri di Fibonacci
F(3)
– Modello a base di molte dinamiche
evolutive delle popolazioni
• F = {f0, ..., fn}
F(1)
+
F(2)
F(1)
+
F(0)
1
1
1
– f0 = 1
casi base (due !)
– f1 = 1
1 passo induttivo
– Per n > 1, fn = fn–1 + fn–2
• Notazione "funzionale": F(i) = fi
- 17. Numeri di Fibonacci in C++
int fibo(int n) {
if (n == 0 || n == 1)
return 1;
else
return (fibo(n - 1) + fibo(n - 2));
}
Ovviamente supponiamo che n>=0
- 18. Un altro esempio: MCD à-la-Euclide
• Il MCD tra M e N (M, N naturali positivi)
– se M=N allora MCD è N
1 caso base
– se M>N allora esso è il MCD tra N e M-N
– se N>M allora esso è il MCD tra M e N-M
30
2 passi induttivi
18
12
12
18
6
6
6
- 19. MCD: iterativo & ricorsivo
int euclideIter(int m, int n)
{
while( m != n )
if ( m > n )
m = m – n;
else
n = n – m;
return m;
}
int euclideRic (int m, int n)
{
if ( m == n )
return n;
if ( m > n )
return euclideRic(m–n, n);
else
return euclideRic(m, n–m);
}
- 20. Funzione esponenziale (intera)
• Definizione iterativa:
– 1) xy = 1 se y = 0
– 2) xy = x * x * … x
(y volte) se y > 0
• Codice iterativo:
int esp (int x, int y) {
int e = 1;
for (int i = 1; i <= y; i++ )
e *= x;
return e;
}
• Codice ricorsivo:
• Definizione ricorsiva:
– 1) xy = 1 se y = 0
– 2) xy = x * x(y-1)
se y > 0
int esp (int x, int y) {
if ( y == 0 )
return 1;
else
return x * esp(x, y-1);
}
- 21. Ricorsione e passaggio per reference
(incrementare m volte una var del chiamante)
void incrementa(int &n, int m) {
if (m != 0) {
n++;
incrementa(n, m - 1);
}
}
int main() {
cout << "Inserire due numeri" << endl;
int numero, volte;
cin >> numero >> volte;
incrementa(numero, volte);
cout << numero;
return 0;
}
• n è un sinonimo della
variabile del chiamante… ciò
vale in modo ricorsivo ..
• Per cui n si riferisce sempre
alla variabile numero del
main()
- 23. Terminazione (ancora!)
• Attenzione al rischio di catene infinite di
chiamate
• Occorre che le chiamate siano soggette a una
condizione che prima o poi assicura che la
catena termini
• Occorre anche che l'argomento sia
"progressivamente ridotto" dal passo
induttivo, in modo da tendere prima o poi al
caso base
- 24. Costruzione di una stringa invertita
• Data un stringa s1 produrre una seconda
stringa s2 che contiene i caratteri in ordine
inverso
A
B
C
B
C
+
C
A
+
B
+
A
- 25. Costruzione di una stringa invertita
string inversione(string s) {
// caso base
if (s.size() == 1)
return s;
// passo induttivo
return
inversione(s.substr(1,s.size()-1))
+ s[0];
}
string substr (size_t pos = 0, size_t
len = npos) const;
• Restituisce una nuova stringa
costruita con len caratteri a
partire da pos
• http://www.cplusplus.com/ref
erence/string/string/substr/
int main() {
string s1 = "Hello world!!";
string s2;
s2 = inversione(s1);
cout << s2;
return 0;
}
• NB: Soluzione non ottimale
che crea una stringa
temporanea per ogni carattere
della stringa da invertire
- 26. Palindromi in versione ricorsiva
• Un palindromo è tale se:
• la parola è di lunghezza 0 o 1;
– oppure
Caso base
• il primo e l'ultimo carattere della parola sono
uguali e inoltre la sotto-parola che si ottiene
ignorando i caratteri estremi è a sua volta un
palindromo
Passo induttivo
• Il passo induttivo riduce la dimensione del
problema!
- 28. Codice
bool palindroma(string par, int da, int a) {
if (da >= a)
return true;
else
return (par[da] == par[a] &&
palindroma(par, da+1, a-1));
}
• Notare la regola del
cortocircuito
• Evita il passo ricorsivo se si
trovano due caratteri
discordi
int main() {
string parola;
cout << "Inserisci la parola" << endl;
cin >> parola;
bool risultato =
palindroma(parola,0,parola.size()-1);
if (risultato)
cout << "La parola " << parola
<< " è palindroma" << endl;
else
cout << "La parola " << parola
<< " NON è palindroma" << endl;
return 0;
}
• Notare che il primo passo
richiede di inizializzare la
ricorsione con i valori
degli estremi di partenza
- 29. Ricerca Binaria
• Scrivere un programma che implementi
l’algoritmo di ricerca dicotomica in un vettore
ordinato in senso crescente, con procedimento
ricorsivo.
• Dato un valore val da trovare e un vettore array
con due indici low, high, che puntano
rispettivamente al primo e ultimo elemento;
– L’algoritmo di ricerca dicotomica prevede che se
l’elemento f non è al centro del vettore cioè in
posizione “m = (low+high)/2” allora deve essere
ricercato ricorsivamente soltanto in uno dei due sottovettori a destra o a sinistra dell’elemento centrale
- 30. Progettazione
• Se low > high, allora l’elemento cercato f non è
presente nel vettore
(caso base)
• Se (val == array [ (low+high) / 2 ]), allora f è
presente nel vettore. (caso base)
• Altrimenti (passo induttivo)
– Se (f > array[ (low+high) / 2 ]) la ricerca deve
continuare nel sottovettore individuato dagli elementi
con indici nell’intervallo *m +1, high+
– Se (f < array[ (low+high) / 2 ]) allora la ricerca deve
continuare nel sottovettore individuato dagli elementi
con indici nell’intervallo *low, m - 1]
- 31. Codice
bool BinarySearch(int array[], int
low, int high, int val) {
int m;
if (low > high)
return false;
else {
m = (low + high) / 2;
if (val == array[m])
return true;
else if (val > array[m])
return BinarySearch(array,
m + 1, high, val);
else
return BinarySearch(array,
low, m - 1, val);
}
}
int main() {
int sequenza[] =
{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int valore;
cin >> valore;
bool trovato =
BinarySearch(sequenza,0,size-1,valore);
if (trovato)
cout << "Risultato trovato " << endl;
else
cout << "Risultato non presente"
<< endl;
return 0;
}
- 32. Le torri di Hanoi
Stampare le mosse necessarie per spostare tutta la torre da A a C
muovendo un cerchio alla volta e senza mai mettere un
cerchio più grosso su uno più piccolo
Torre di
n
dischi
A
B
C
FORMULAZIONE RICORSIVA?
- 33. Le torri di Hanoi
FORMULAZIONE RICORSIVA
Torre di
n-1
dischi
A
33
B
C
- 37. Progettazione ricorsiva
• Spostare la torre di 1 elemento da non viola
mai le regole e si effettua con un passo
elementare (caso base)
• Per spostare la torre di N elementi, p.e. da A a
C
– sposto la torre di N-1 cerchi da A a B (ricorsione)
– sposto il cerchio restante in C
– sposto la torre di N-1 elementi da B a C
(ricorsione)
- 39. Algoritmo
• Se devi spostare una piramide alta N da x a y
transitando da z
• Sposta una piramide alta N-1 da x a z,
transitando per y
• Sposta il disco N-esimo da x a y
– stampa la mossa
• Sposta una piramide alta N-1 da z a y,
transitando per x
- 40. Codice
void hanoi (int altezza, char da, char a,
char transito) {
if (altezza > 0) {
hanoi (altezza-1, da, transito, a);
cout << "Sposta cerchio da "
<< da << " a "<< a <<endl;
hanoi (altezza-1, transito, a, da);
}
}
int main() {
hanoi (3, 'A', 'C', 'B');
return 0;
}
- 41. Hanoi: soluzione iterativa
• Non è così evidente…
• Stabiliamo un "senso orario" tra i pioli: 1, 2, 3
e poi ancora 1, ecc.
• Per muovere la torre nel prossimo piolo in
senso orario bisogna ripetere questi due passi:
– sposta il disco più piccolo in senso orario
– fai l'unico altro spostamento possibile con un altro
disco
- 42. Ricorsione o iterazione?
•
•
•
•
Spesso le soluzioni ricorsive sono eleganti
Sono vicine alla definizione del problema
Però possono essere inefficienti
Chiamare un sottoprogramma significa
allocare memoria a run-time
N.B. è sempre possibile trovare un
corrispondente iterativo di un
programma ricorsivo
- 43. Calcolo numeri di fibonacci
int fibo(int n) {
if (n == 0 || n == 1)
return 1;
else
return (fibo(n - 1) + fibo(n - 2));
}
• Drammaticamente inefficiente!
• Calcola più volte l'i-esimo numero di
Fibonacci!
- 44. Soluzione con memoria di supporto
• La prima volta che calcolo un dato numero di
Fibonacci lo memorizzo in un array
• Dalla seconda volta in poi, anziché ricalcolarlo,
lo leggo direttamente dall'array
• Mi occorre un valore "sentinella" con cui
inizializzare l'array che mi indichi che il
numero di Fibonacci corrispondente non è
ancora stato calcolato
– Qui posso usare ad esempio 0
- 45. Codice
long fib(int n, long memo[]) {
if (memo[n] != 0)
return memo[n];
memo[n] = fib(n-1,memo) +
fib(n-2, memo);
return memo[n];
}
•
•
•
•
const int MAX = 10;
int main() {
int n;
long memo[MAX];
for (int i = 2; i < MAX; i++)
memo[i] = 0;
memo[0] = 1;
memo[1] = 1; // casi base
cout << "Inserire intero: " << endl;
cin >> n;
cout << "fibonacci di " << n
<< " = " << fib(n, memo);
return 0;
}
Drastica riduzione della complessità (aumento di efficienza)
Questa soluzione richiede un tempo lineare in n
La soluzione precedente richiede un tempo esponenziale in n
Il prezzo è il consumo di memoria in qtà proporzionale a N
- 46. Check this out
• http://stackoverflow.com/questions/360748/c
omputational-complexity-of-fibonaccisequence