1
2 Una ricorsione si definisce non lineare quando si ha più di una chiamata ricorsiva per blocco Per capire bene come opera una ricorsione non lineare si può tracciare un albero che mostra la successione delle chiamate o un albero che mostra la storia dello stack. Una ricorsione si definisce lineare quando si ha al massimo una chiamata ricorsiva per blocco Ricorsione lineare e non lineare
3 Supponiamo di avere il seguente codice: // MAIN int main() { int N; cout<<"main chiama A"<<endl; problemaA(1); cout<<"A ritorna a main"<<endl; cout<<"main chiama C(1)"<<endl; problemaC(1); cout<<"C(1) ritorna a main"<<endl; cout<<"main chiama Fine"<<endl; cout<<" Fine"<<endl; system("pause"); } void problemaA(int N) { cout<<"A chiama B"<<endl; problemaB(N); cout<<"B ritorna A"<<endl;} void problemaB(int N) { cout<<"C("<<N<<") chiama C("<<N-1 <<") “<<endl; cout<<"B chiama C(1)"<<endl; problemaC(1); cout<<" C(1) ritorna a B )"<<endl;} void problemaC(int N){ if (N>0) {cout<<"C("<<N<<") chiama C("<<N-1<<")"<<endl; problemaC(N-1); cout<<" C("<<N-1<<") ritorna a C("<<N<<")"<<endl; } }
4 problema Aproblema Bproblema C(1) problema C(0) MAIN problema C(1) problema C(0) Possiamo rappresentarlo mediante il grafico di figura
5 main chiama A A chiama B B chiama C(1) C(1) chiama C(0) C(0) ritorna a C(1) C(1) ritorna a B B ritorna A A ritorna a main main chiama C(1) C(1) chiama C(0) C(0) ritorna a C(1) C(1) ritorna a main main chiama Fine Fine int main() { int N; cout<<"main chiama A"<<endl; problemaA(1); cout<<"A ritorna a main"<<endl; cout<<"main chiama C(1)"<<endl; problemaC(1); cout<<"C(1) ritorna a main"<<endl; cout<<"main chiama Fine"<<endl; cout<<" Fine"<<endl; system("pause"); } // DEFINIZIONI void problemaC(int N) { if (N>0) { cout<<" C("<<N<<") chiama C("<<N-1<<")"<<endl; problemaC(N-1); cout<<" C("<<N-1<<") ritorna a C("<<N<<")"<<endl; } void problemaB(int N) { cout<<" B chiama C(1)"<<endl; // cout<<" C("<<N<<") chiama C("<<N-1<<")"<<endl; problemaC(1); cout<<" C(1) ritorna a B )"<<endl; } void problemaA(int N) { cout<<" A chiama B"<<endl; problemaB(N); cout<<" B ritorna A"<<endl; } A sinistra è possibile leggere l’output generato dal codice posto a destra
6 main C(0) ritorna a C(1) C(1) ritorna a BB ritorna A A ritorna a main C(1) ritorna a mainC(0) ritorna a C(1) main chiama A A A chiama B B B chiama C(1) C(1) C(1) chiama C(0) C(0) Fine ritorna a main main chiama C(1) C(1) C(1) chiama C(0) C(0) main chiama Fine Rappresentiamo le chiamate alle varie function con il grafico di figura
7 C(0) main A B C(1) C(0) fine Allegato: La ricorsione non lineare Più sinteticamente rappresentato dall’albero seguente:
8 Intorno all’anno 1170 a Pisa un commerciante di nome Bonaccio Pisano ebbe un figlio che chiamò Leonardo. Per motivi di lavoro si trasferì presto a Algeri dove Leonardo crebbe, studiò e imparò l’arabo avendo dei precettori musulmani. Da costoro imparò la matematica indo-arabica, il sistema decimale con la notazione posizionale delle cifre e l’uso dello zero. Leonardo Pisano scrisse il primo libro di matematica occidentale il “Liber Abaci” introducendo le cifre arabe Affascinato dal mondo e dalla cultura araba dove ogni persona ha nel suo cognome anche il nome del padre aggiunse al suo nome di Leonardo il cognome Filius Bonacci trasformato per brevità in Fibonacci. La successione di Fibonacci
9 La copia del Liber Abaci posseduta dalla Biblioteca Nazionale di Napoli
10 Problema Nel 1228 messer Leonardo Pisano detto Fibonacci si pose il seguente problema: posta una coppia di conigli in un recinto supponiamo che questa coppia dopo due mesi generi un’altra coppia e successivamente ne generi una al mese. La seconda coppia a sua volta dopo altri due mesi ne genera un’altra e così via. Dopo un anno, cioè dopo 12 mesi quante coppie di conigli ci saranno nel recinto?
La successione di Fibonacci 10
12 La successione di numeri interi che così si ottiene è detta successione di Fibonacci. E’ possibile rinvenire tale successione in molti esempi presenti in natura. Nella diapositiva successiva è mostrato il caso delle spirali dei girasole e dei cammini che un ape può percorrere per andare dalla prima cella di un alveare alla n-esima, supponendo che tutte le celle siano di forma esagonale.
13 ALCUNI CASI STRANI L’ape può raggiungere la cella n seguendo FIB(n+1) percorsi diversi Girasole con 53 (FIB(10)) spirali antiorarie e 89 (FIB(11)) orarie
14 La funzione FIB(N) che calcola i numeri di Fibonacci relativa ad un intero N obbedisce ad una regola molto semplice che si può descrivere in modo ricorsivo: Se N=0 allora FIB(N)=0 Se N=1 allora FIB(N)=1 Se N>1 allora FIB(N)=FIB(N-1)+FIB(N-2) La funzione FIB(N) si presta ad essere subito tradotta nel seguente codice Osserviamo che in una stessa riga di codice abbiamo due chiamate ricorsive (FIB(N-1)+FIB(N-2)) int FIB(int N) { if (N==0) return 0; else if (N==1) return 1; else return FIB(N-1)+FIB(N-2); }
15 Siamo di fronte ad una ricorsione non lineare quando nell’ambito di uno stesso processo ricorsivo si ha più di una chiamata ricorsiva. Pur nella sua semplicità, l’algoritmo mostrato consuma molte risorse: per numeri grandi c’è il pericolo di uno stack overflow. Per mostrare questo aspetto, introduciamo una nuova variabile, chiama, che ci restituisca il numero di volte in cui la funzione richiama se stessa; tale variabile, passata per riferimento, viene aggiunta alla lista dei parametri formali. int FIB(int N, int &chiama) { chiama++; if (N==0) return 0; else if (N==1) return 1; else return FIB(N-1, chiama)+FIB(N-2, chiama); }
16 Come precedentemente introdotto è possibile rappresentare l’evoluzione di un processo ricorsivo mostrando l’albero delle successive chiamate ricorsive. Quest’albero è mostrato nel lucido che segue per FIB(4). Nel lucido succesivo è mostrato l’albero per FIB(6) e una corrispondente rappresentazione dello stack relativo ai processi ricorsivi man mano aperti.
17 main F(3) F(1) F(2) F(4) F(2) Fibonacci(4) F(1)F(0) F(1)F(0) main F(3) F(1)F(2) F(1)F(0) Fibonacci(3) int FIB(int N, int &chiama) { chiama++; if (N==0) return 0; else if (N==1) return 1; else return FIB(N-1, chiama)+FIB(N-2,chiama); } Albero delle chiamate ricorsive di FIB(3) e FIB(4)
18 F(6) F(5)F(4) + F(2) F(3) + F(2) F(1) + F(0)F(1) F(3) + F(0)F(1) F(3) F(1) F(2) F(4) F(5) F(3) F(6) F(1)F(2) F(3) F(4) F(2) F(0)F(1) F(0) F(1) F(0)F(1) F(0) F(1) int FIB(int N) { if (N==0) return 0; else if (N==1) return 1; else return FIB(N-1)+FIB(N-2); } Albero delle chiamate ricorsive di FIB(6) stack
19 Complessità dell’algoritmo per il calcolo dei numeri di Fibonacci E’ stato dimostrato che per N abbastanza grande la complessità di F(N), cioè il numero di chiamate ricorsive, è circa O(1.61 N ). N1,61^N30 MFLOP 1 TERAFLOP sec ,3sec ,2min anni E+26anni3E+23
20 // La successione di fibonacci ricorsiva #include using namespace std; // PROTOTIPI double FIB(int, double &); // MAIN int main() { double N;double Tot; do { cout<<"Assegna N "<<endl; cin>>N; Tot=0; cout<<" Il numero di fibonacci "<<N<<" = "<< FIB(N,Tot)<<"chiamate = "<< Tot <<endl; } while (N>=0); system("pause"); } // DEFINIZIONI double FIB(int N, double &chiama) { chiama++; if (N==1) return 1; else if (N==2) return 1; else return FIB(N-1,chiama)+FIB(N-2,chiama); } Il codice che segue mostra il calcolo della successione di Fibonacci per un preassegnato valore e il numero di chiamate ricorsive necessarie.
21 Fibonacci(5) = 5 chiamate= 9 Fibonacci(10) = 55 chiamate= 109 Fibonacci(15) = 610 chiamate= Fibonacci(20) = chiamate= Fibonacci(25) = chiamate= Fibonacci(30) = chiamate= Fibonacci(35) = chiamate= Fibonacci(40) = chiamate= Fibonacci(45) = chiamate= Output del codice precedente per diversi valori di N.
22 Per ridurre da O (1.61 N ) a O(N) la complessità di calcolo per i numeri di Fibonacci invece di chiamare ricorsivamente la procedura di calcolo per ogni nodo dell’albero dello stack depositiamo i risultati di ogni computazione in un array e li richiamiamo, senza più calcolarli ogni volta che ne abbiamo bisogno. Detto FibNos l’array in cui si depositano i numeri parziali possiamo costruire una procedura ricorsiva alla seguente maniera: caso base Quando N=2 allora poni FibNos[0] 0 FibNos[1] 1 FibNos[2] 1 CHIAMATA RICORSIVA FibNos[N] FibNos[N-2] +FibNos[N-1]
23 void FibVett(int N, double FibNos[]) { if (N==2) { FibNos[0] =0; FibNos[1] =1; FibNos[2] =1; } else FibVett(N-1,FibNos); FibNos[N]=FibNos[N-2] + FibNos[N-1]; Svantaggio Siamo limitati dalla dimensione dell’array per determinare N massimo F(3) F(1) F(2) F(4) F(2) FibVetNo(4) FibVetNo(3) FibVetNo(2) FibNos[4]=3 FibNos[0]=0 FibNos[1]=1 FibNos[2]=1 FibNos[3]=2 Di seguito mostriamo il codice, l’albero ricorsivo, e lo stack delle chiamate fibonacci
24 double fibo(double N, double prec, double att, double i, double &chiama) { double temp; if (i<N) { temp=prec+att; prec=att; att=temp; chiama++; return fibo(N,prec,att,i+1,chiama); } else return att; } int main () { double Num; cout<<" Quale numero di fibonacci desideri ? "; cin>>Num; double i=2, p=1, chiama=0; cout<<" Il fibonacci di "<<Num<<" e' "<<fibo(Num,p,a,i,chiama)<<" passi “<<chiama << endl; system("pause"); } Un’altra soluzione sempre di complessità lineare sfrutta due variabili di appoggio ed è illustrata di seguito.
25 N=6, prec=1, att=1, i=2 N=6, prec=1, att=2, i=3 N=6, prec=2, att=3, i=4 Allegato: fibonacci Lo stack ricorsivo di fibo(6) double fibo(double N, double prec, double att, double i, double &chiama) { double temp; if (i<N) { temp=prec+att; prec=att; att=temp; chiama++; return fibo(N,prec,att,i+1,chiama); } else return att; } N=6, prec=3, att=5, i=5 N=6, prec=5, att=8, i=6att=8 +
26 ESERCIZIO Detto F n l’ennesimo numero di Fibonacci scrivere un programma che calcoli l’espressione: A=F n+1 * F n-1 - F 2 n Mostrare i valori di A per N=1,2,3,4,5,6,7,8,9,10 Allegato: fibondiff
27 Esercizio 1- Assegnato un intero N, calcolare il rapporto tra le coppie di numeri di Fibonacci F(n+1)/F(n) e per ogni coppia sottrarre a detto rapporto la radice positiva dell’equazione x 2 -x-1=0. 2- Calcolare il MCD tra la somma dei numeri di Fibonacci compresi tra 1 e 10 e quelli compresi tra 11 e 20 compresi tra 11 e 20 e quelli compresi tra 21 e 30 compresi tra 21 e 30 e quelli compresi tra 31 e 40 Testo consigliato da leggere MARIO LIVIO – La sezione aurea
28 Calcolare con una funzione ricorsiva l’N-esimo termine della successione seguente a1=3 a2=1 a3=-1 a n =a n-1 *a n-2 -(a n-3 +a n-1 +a n-2 ) Caso base -> S(1)=3, S(2)=1, S(3)=-1 Chiamata ricorsiva -> S(N)=S(N-1)*S(N-2)-(S(N-3)+S(N-1)+S(N-2)) ESERCIZIO Di seguito è riportato il codice corrispondente.
29 /*Calcolare con una funzione ricorsiva l’N-esimo termine della successione seguente a1=3 a2=1 a3=-1 an=an-1*an-2-(an-3+an-1+an-2) */ double succ(double N) { if (N==1) return 3; else if (N==2) return 1; else if (N==3) return -1; else return succ(N-1)*succ(N-2)-(succ(N-3)+succ(N-1)+succ(N-2));
30 Riscrivere la procedura precedente rendendola lineare