Svolgimento Esercizio 1
Svolgimento Esercizio 2
Sappiamo che esiste una costante
ed un
tale che
Pertanto,
per ogni
,
Ciò dimostra che
.
Svolgimento Esercizio 3
Si noti che, per ogni ,
Riponendo le funzioni proposte secondo tale ordine, abbiamo
![]() |
![]() |
![]() |
|
![]() |
![]() |
![]() |
|
![]() |
![]() |
![]() |
|
![]() |
![]() |
![]() |
|
![]() |
![]() |
![]() |
Da cui risulta evidente che
![]() |
![]() |
![]() |
|
![]() |
![]() |
![]() |
|
![]() |
![]() |
![]() |
|
![]() |
![]() |
![]() |
Volendo verificarlo ad un più basso livello, abbiamo infatti che
L'unico caso critico è il confronto tra
ed
che non differiscono per una costante moltiplicativa.
La differenza nel loro ordine di crescita
asintotico può essere messa in evidenza utilizzando
il teorema dell'Hôpital, ed osservando che
I restanti due casi sono banali, simili al primo, e del tutto analoghi tra di loro. Ne affronteremo pertanto solo uno, e lasciamo l'altro come esercizio mentale.
poichè
.
Svolgimento Esercizio 4
Avevamo visto a lezione che
.
Come prima cosa,
riconduciamoci pertanto a quanto per noi già noto.
A tal fine, basta una piccola manipolazione algebrica:
Questa era già una risposta parziale all' esercizio,
ma per completarlo, dobbiamo mostrare anche che
.
Chiaramente,
si consideri la seguente procedura iterativa.
#include<iostream.h> int main() { int n; cout << "Dammi n: "; cin >> n; cout << endl; long long int T[n +1]; T[0] = 1; cout << "T[0] = " << T[0] << endl; for(int k=1; k<=n; k++) { T[k] = T[k-1]+T[k-1]; cout << "T[" << n << "] = " << T[n] << endl; } }
Si stabilisca l'ordine di crescita del tempo di calcolo della procedura proposta. Si stabilisca l'ordine di crescita della memoria impiegata dalla procedura proposta.
Svolgimento Esercizio 5
La memoria occupata è sicuramente ,
causa l'allocazione long long int T[n +1]
alla seconda linea del codice proposto.
Infatti, tale linea viene eseguita incondizionatamente,
ad ogni immissione di un input corretto.
La memoria occupata è di fatto ,
poichè se andiamo a considerare le altre allocazioni di memoria
esplicitamente od implicitamente comportate dal nostro codice,
risulta evidente che, per ogni realizzazione ragionevole di un compilatore,
esse saranno in numero costante
, e non dipenderanno cioè
dal valore
in input.
Il tempo di calcolo impiegato è sicuramente ,
causa il ciclo for(int k=1; k<=n; k++)
presente alla quarta linea del codice proposto.
Infatti tale ciclo for viene eseguito incondizionatamente, e per intero,
ad ogni immissione di un input corretto.
Il tempo di calcolo impiegato è di fatto ,
poichè le istruzioni contenute
nel ciclo for(int k=1; k<=n; k++)
sono eseguite
volte,
mentre quelle esterne al ciclo sono eseguite
volta.
Inoltre, tutte le istruzioni, è ragionevole aspettarsi,
richiedano
.
A dire il vero, per l'istruzione,
long long int T[n +1],
uno potrebbe chiedersi se essa non possa richiedere più di
tempo.
Tuttavia, è ragionevole ipotizzare quantomeno che
il tempo richiesto da tale istruzione sia
,
e si noti che tale istruzione è esterna al ciclo for
ed è pertanto eseguita un'unica volta.
#include<iostream.h> long long int T(int n) { if(n==0) return 1; return T(n-1) + T(n-1); } int main() { int n; cout << "Dammi n: "; cin >> n; cout << endl; cout << "T[" << n << "] = " << T(n) << endl; }
Si stabilisca l'ordine asintotico di crescita per il tempo di calcolo della procedura proposta dal professor Tortoise. Si stabilisca l'ordine di crescita della memoria impiegata dalla procedura di Tortoise.
Svolgimento Esercizio 6
Per il tempo di calcolo, possiamo scrivere la seguente ricorrenza,
Ovviamente,
.
Possiamo infatti verificare per sostituzione sia che
:
sia che
:
La contraddizione
è solo apparente ed è dovuta al fatto
che abbiamo lavorato prescindendo dalla base dell'induzione.
Per quanto riguarda l'occupazione di memoria dobbiamo prestare
attenzione al fatto che ad ogni chiamata di procedura
viene allocata una certa quantità di memoria.
Nel caso della nostra semplice procedura
long long int T(int n), tale quantità di memoria risulta essere .
Quando la procedura termina, tale quantità di memoria viene però rilasciata.
E tuttavia singole quantità di memoria allocate
a fronte di chiamate diverse vengono a sommarsi se la seconda chiamata
avviene prima che la prima chiamata sia già stata esaurita.
Siamo cioè chiamati a valutare l'altezza dell'albero di ricorsione,
che corrispende al massimo numero di ambienti che dovranno essere mantenuti aperti
contemporaneamente.
Nel nostro caso, l'altezza dell'albero di ricorsione risulta essere
.
Pertanto, l'occupazione di memoria sarà
.
Complemento 6+
Per questa particolare ricorrenza (
),
è possibile pensare ad un accorgimento
che consenta di ottenere un buon tempo di calcolo anche
per la procedura ricorsiva.
Sapresti indicare quale?
Svolgimento Complemento 6+
Si vede che la procedura può facilmente evitare di fare due chiamate ricorsive. Basta infatti sostituire, internamente alla procedura ricorsiva long long int T(int n), la riga di codice
return T(n-1) + T(n-1);
con la riga di codice
return 2*T(n-1);
così che la ricorrenza per il tempo di calcolo diviene
portando quindi ad un tempo di calcolo che sarà
come è facile evidenziare srotolando la ricorrenza.
Purtroppo, tale modifica non migliora la situazione per quanto concerne l'occupazione di memoria.
Sapresti, come ulteriore esercizio,
modificare la versione iterativa di cui all'Esercizio 5,
ottenendo una soluzione algoritmica che richieda
solamente come occupazione di memoria,
senza per altro peggiorare il running time
(anzi, di fatto migliorando anche esso,
anche se non dal punto di vista asintotico)?