Chiara Bodei, Jacopo SoldaniDipartimento di InformaticaUniversitร of Pisa
A1. Introduzione agli AlgoritmiFondamenti di Programmazione e Laboratorio
Algoritmi: Definizione Informale
2
Es. algoritmo CuociLaPasta
1. Riempi la pentola dโacqua
2. Metti la pentola sul fornello
3. Accendi il fornello
4. Attendi che lโacqua sia arrivata ad ebollizione
5. Aggiungi il sale
6. Butta la pasta
7. Attendi il tempo di cottura della pasta
8. Scola la pasta
9. Spegni il fornello
Un algoritmo รจ un insieme di istruzioni, definite passo per passo, che possono essere riprodotte meccanicamente
per determinare il risultato desiderato
Algoritmi: Perchรฉ? Come?
3
Gli algoritmi costituiscono una delle basi della programmazione
โ Forniscono il procedimento per giungere alla soluzione di un dato problema di calcolo
Gli algoritmi saranno descritti ad alto livello, utilizzando il cosiddetto pseudocodice
โข Lo pseudocodice ricorda linguaggi di programmazione reali (come il C), ma
โข puรฒ contenere anche frasi in linguaggio naturale.
NB: La traduzione in un particolare linguaggio di programmazione (come C) puรฒ essere fatta in modo quasi meccanico.. provateci! :)
Correttezza ed Efficienza degli Algoritmi
4
Un algoritmo deve essere (i) corretto ed (ii-iii) efficiente, ovvero deve
i. produrre correttamente il risultato desiderato,
ii. essere "veloce" (efficiente in termini di tempo di esecuzione), e
iii. essere "economico" (efficiente in termini di occupazione di memoria)
(ii) e (iii) determinano la complessitร in tempo e spazio dellโalgoritmo
Come Analizzare gli Algoritmi, e Perchรฉ Farlo
5
Come analizzare correttezza e complessitร degli algoritmi?
โข Analisi teorica (tramite dimostrazioni di correttezza e complessitร )
โข Analisi sperimentale (tramite lโanalisi di possibili istanze di dati su cui lโalgoritmo opera)
Analisi teorica (vs. analisi sperimentale)
โข piรน affidabile, poichรฉ vale su tutte le possibili istanze di dati su cui lโalgoritmo puรฒ operare
โข semplifica il confronto tra diverse soluzioni algoritmiche per uno stesso problema
โข consente la predizione delle prestazioni di un programma software che implementi effettivamente un algoritmo
Fibonacci: Un Problema Notoโฆ..e Sei Diversi Algoritmi per Risolverlo
6
I Conigli di Fibonacci
7
Domanda: Quante coppie di conigli si avrebbero allโanno ๐, partendo da una singola coppia di conigli?
Condizioni (aka. assunzioni):
โข Ogni coppia di conigli fertili genera due nuovi coniglietti ogni anno
โข I conigli risultano fertili solamente dal secondo anno dopo la loro nascita
โข I conigli sono immortali
Problema: Quanto velocemente si espanderebbe una popolazione di conigli in unโisola deserta e sotto appropriate condizioni?
I Conigli di Fibonacci: Tasso di Riproduzione
8
Sotto tali condizioni, e partendo da una singola coppia di coniglietti, si ottiene quanto segue
โข ๐น1 = 1 // una sola coppia di conigli
โข ๐น2 = 1 // troppo giovani per riprodursi
โข ๐น3 = 2 // una coppia si riproduce
โข ๐น4 = 3 // una coppia si riproduce, mentre lโaltra รจ troppo giovane
โข ๐น5 = 5 // due coppie si riproducono, una รจ troppo giovane
โข โฆ
dove
๐น๐ : numero di coppie di conigli presenti nellโanno ๐
Domanda: Qual รจ la regola di espansione della popolazione?
La Regola di Espansione dei Conigli di Fibonacci
9
Nellโanno ๐ ci sono le seguenti coppie di conigli:
โข tutte le coppie dellโanno precedente e
โข una nuova coppia di coniglietti per ogni coppia fertile (ovvero giร presente due anni prima)
Continuando ad indicare con ๐น๐ il numero di coppie di conigli presenti nellโanno ๐, abbiamo quindi la seguente relazione di ricorrenza:
๐น๐ = แ๐น๐โ1 + ๐น๐โ2 ๐ โฅ 31 ๐ = 1,2
Problema: Come calcoliamo ๐น๐?
Approccio Numerico: Fib1
10
Lโennesimo numero della sequenza di Fibonacci si puรฒ calcolare in funzione della sezione aurea ๐, grazie alla formula di Binet
๐น๐ =๐๐ โ โ๐ โ๐
5
Problema: ๐ รจ un numero irrazionale, non rappresentabile nella memoria di un calcolatore
Soluzione: Approssimare la sezione aurea ๐ con un valore rappresentabile, es 1.618
๐น๐ =1,618๐ โ โ1,618 โ๐
5
Lโalgoritmo Fib1
11
int Fib1(int n) {
return1,618๐โ โ1,618 โ๐
5;
}
Domanda: Lโalgoritmo Fib1 รจ corretto?
Correttezza di Fib1
12
Lโaccuratezza di una soluzione come quella dellโalgoritmo Fib1 dipende strettamente da come/quanto sono approssimati i valori non rappresentabili (come ๐, ad esempio)
Ad esempio, approssimando ๐ con 3 cifre decimali (ovvero usando 1.618 al posto di ๐) si ha che
โฆ quindi (un approccio come quello in) Fib1 non puรฒ essere considerato corretto.
n 1,618๐โ โ1,618 โ๐
5Fib1(n) ๐น๐
3 1.99992 2 2
16 986.698 987 987
18 2583.1 2583 2584
Domanda: E se usassimo direttamente la definizione ricorsiva di ๐น๐?
Approccio Ricorsivo: Fib2
13
NB: Operando solamente con numeri interi, non presenta il problema dellโapprossimazione.
โ Calcola correttamente lโennesimo numero della sequenza di Fibonacci!
int Fib2(int n) {
if n<=2 then return 1;
else return Fib2(n-1) + Fib2(n-2);
}
Domanda: Quanto tempo richiede Fib2?
Misurare la Complessitร in Tempo
14
Come misurare il tempo impiegato da un algoritmo?
โข In secondi?
(mmmโฆdipende dalla piattaforma di esecuzioneโฆ)
โข In numero di istruzioni macchina?
(mmmโฆdipende dal compilatoreโฆ)
โข Altre idee?
In prima approssimazione, consideriamo
โข il numero di linee di pseudocodice mandate in esecuzione
(indipendente da piattaforma/compilatoreโฆ)
Tempo di Esecuzione di Fib2
15
Numero di linee di codice mandate in esecuzione da Fib2:
โข n=1 โ 1 linea di codice
โข n=2 โ 1 linea di codice
โข n=3 โ 2 linee per Fib2(3), 1 per Fib2(2), e 1 per Fib2(1)
โข n=4 โ 2 linee per Fib2(4), 2 per Fib2(3), e 1 per Fib2(2)
โข โฆ
Osservazione: Quando n>=3
โข si eseguono due linee di codice,
โข oltre a quelle eseguite nelle chiamate ricorsive
Relazione di Ricorrenza
16
Per Fib2, quindi, si ha che๐(๐) = 2 + ๐(๐ โ 1) + ๐(๐ โ 2)
In generale: il tempo richiesto da un algoritmo ricorsivo รจ pari al tempo speso allโinterno della chiamata
piรน quello speso nelle chiamate ricorsive
Domanda: Come calcolare ๐(๐)?
Albero di Ricorsione
17
La ricorsione puรฒ essere visualizzata mediante il cosiddetto albero di ricorsione
โข nodi โ attivazioni della funzione
โข archi โ chiamate ricorsive
Ad esempio
Fib2(6)
Fib2(5) Fib2(4)
Fib2(3) Fib2(2)
Fib2(2) Fib2(1)
Fib2(4)
Fib2(3) Fib2(2)
Fib2(2) Fib2(1)
Fib2(3)
Fib2(2) Fib2(1)
Calcolare ๐ ๐
18
Intuitivamente, il valore di ๐(๐) puรฒ essere calcolato
โข assegnando "pesi" i nodi dellโalbero con il numero di linee di codice eseguite nella chiamata corrispondente,
- i nodi interni "pesano" 2 istruzioni
- i nodi foglia "pesano" 1 istruzione
โข contando i numeri di nodi interni e di foglie, e
โข moltiplicandoli per il loro peso
Calcolare ๐ ๐ (2)
19
Osservazione 1: Il numero di foglie dellโalbero di ricorsione di Fib2(n) รจ pari a ๐น๐
Osservazione 2: Il numero di nodiinterni in un albero in cui ogninodo ha 2 figli รจ pari alnumero di foglie โ 1
In totale, le line di codice eseguite sono๐ ๐ = ๐น๐ + 2 โ (๐น๐ โ1) = 3๐น๐ โ 2
Fib2(6)
Fib2(5) Fib2(4)
Fib2(3) Fib2(2)
Fib2(2) Fib2(1)
Fib2(4)
Fib2(3) Fib2(2)
Fib2(2) Fib2(1)
Fib2(3)
Fib2(2) Fib2(1)
Fib2(n) รจ lento, visto che ๐ ๐ โ ๐น๐
Una Soluzione Piรน Veloce?
20
Lโalgoritmo Fib2 calcola ripetutamente la soluzione dello stesso sottoproblema
Ad esempio, per Fib2(5)
โข calcola Fib2(4), risolvendo ricorsivamente
โข Fib2(3) e
โข Fib2(2), e poi
โข ricalcola Fib2(3), risolvendo ricorsivamente
โข Fib2(2) e
โข Fib2(1)
Idea: E se ci salvassimo i risultati giร calcolati?
Usare la Memoria: Fib3
21
NB: Sfrutta lโarray F di n interi per evitare di ricalcolare i valori di ๐น๐โ1 e ๐น๐โ2 per ogni ๐ โ [2, ๐)
int Fib3(int n) {
int *F = malloc(n*sizeof(int)); // F รจ un array di n interi
F[0]=F[1]=1;
for (int i=2;i<n;i++)
F[i] = F[i-1] + F[i-2];
return F[n-1];
}
Domanda: Quanto tempo richiede Fib3? Quanta memoria?
Tempo di Esecuzione di Fib3
22
Lโalgoritmo Fib3 "scorre un array di n interi" per calcolare ๐น๐โข Il numero di istruzioni da eseguire cresce proporzionalmente con ๐ e
โข non esponenzialmente come nel caso di Fib2
Hardware Fib2(58) Fib3(58)
Pentium IV1700 MHz
15820 secondi(circa 4 ore)
0,7 milionesimi di secondo
Pentium III450 MHz
43518 secondi(circa 12 ore)
2,4 milionesimi di secondo
PowerPC G4 500MHz
58321 secondi(circa 16 ore)
2,8 milionesimi di secondo
Occupazione di Memoria
23
Il tempo di esecuzione non รจ il solo "costo" (o meglio, la sola risorsa di calcolo) da considerare
โ Lโoccupazione di memoria puรฒ essere altrettanto cruciale
algoritmo lento
algoritmo che occupa piรน memoria di quella disponibile
necessario attendere a lungo per avere la soluzione
desiderata
non calcola mai la soluzione, indipendentemente dal
tempo che ci mette
Risparmiare Memoria: Fib4
24
NB: Fib3 usa un array di n interi, quando basterebbe mantenere solamente gli ultimi due valori di ๐น๐
int Fib4(int n) {
int f1,f2,fcurr;
f1=f2=1;
for (int i=2;i<n;i++) {
fcurr = f1 + f2;
f1 = f2;
f2 = fcurr;
}
return fcurr;
}
NB: Fib4 impiega sempre un numero di istruzioni proporzionale a n, ma richiedendo un numero costante di interi in memoria (invece che una funzione di n)
Come Misurare il Tempo (2)
25
No, รจ una misura molto approssimativa! Ad esempio, possiamo aumentare le linee di codiceโข inserendo commenti oโข andando a capo piรน spesso
senza perรฒ aumentare anche il tempo di esecuzione del programma
Per lo stesso programma, potremmo concludere che โข ๐ ๐ = 15๐ + 3 // senza commenti/accapoโข ๐ ๐ = 350๐ + 5 // con commenti/accapo
Necessaria una notazione che permetta di astrarre da dettagli inessenziali come costanti e costanti moltiplicative..
Domanda: Ha davvero senso misurare ๐(๐) come il numero di linee di codice mandate in esecuzione?
Notazione Asintotica
26
๐ ๐ = ๐ ๐ = ๐(๐ ๐ )
se ๐(๐) < ๐ โ ๐(๐) per
โข una qualche costante ๐ e
โข ๐ sufficientemente grande (ovvero piรน grande di un qualche valore ๐0)
Notazione Asintotica
27
Alcuni esempi
โข ๐(๐) = ๐ + 2 diventa ๐(๐) = ๐(๐)
โข ๐(๐) = 137๐ + 25 diventa ๐(๐) = ๐(๐)
โข ๐(๐) = 2๐ + 14 diventa ๐(๐) = ๐(2๐)
โข ๐(๐) = 2๐ + ๐3 + 3 diventa ๐(๐) = ๐(2๐)
Notazione Asintotica: Fib4
28
Fib4 impiega un numero proporzionale ad n di istruzioni per calcolare ๐น๐
๐๐น๐๐4 ๐ = ๐(๐)
Domanda: Possiamo calcolare ๐น๐ in un tempo inferiore a ๐(๐)?
Metodo delle Potenze: Fib5
29
ร possibile dimostrare per induzione la seguente proprietร di matrici
1 11 0
๐
=๐น๐+1 ๐น๐๐น๐ ๐น๐โ1
Sfruttando questa proprietร si ottiene un nuovo algoritmo
int Fib5(int n) {
M = 1 11 0
;
for (int i=0;i<n-1;i++)
M = M * 1 11 0
;
return M[0][0];
}
Domanda: Il tempo di esecuzione รจ sempre ๐(๐).E quindi? A cosa serve?
Calcolare le Potenze in Modo Furbo
30
Calcoliamo la potenza n-esima di un numero moltiplicando quel numero n volte per se stesso
67 = 6 โ 6 โ 6 โ 6 โ 6 โ 6 โ 6 = 279.936
Possiamo perรฒ procedere anche come segue:
67 = 63 โ 63 โ 6 = 62 โ 6 โ 62 โ 6 โ 6 = 279.936
Ovvero scomporre il calcolo di una potenza ๐ฅ๐ฆ in
โข calcolo di ๐ฅ๐ฆ/2,
โข moltiplicazione ๐ฅ๐ฆ/2 per se stesso e
โข moltiplicazione del risultato per ๐ฅ (solo se ๐ฆ รจ dispari)
Fib6
31
int Fib6(int n) {
int M = 1 11 0
;
potenza(M,n-1);
return M[0][0];
}
void potenza(int Matrice[][2],int esponente) {
if (n>1) {
potenza(Matrice,esponente/2);
Matrice = Matrice * Matrice;
}
if (esponente%2 == 1)
Matrice = Matrice * 1 11 0
;
}
Tempo di Esecuzione di Fib6
32
Dipende strettamente dallโesecuzione di potenza
โข potenza richiede tempo costante per essere eseguita, piรน
โข la chiamata ricorsiva a potenza con input esponente/2
Fib6 quindi rispetta la seguente relazione di ricorrenza
๐ ๐ = แ๐ 1 + ๐
๐
2, ๐ > 1
๐(1), ๐ โค 1
Domanda: Come risolverla?
Metodo dellโIterazione
33
Iterando su ๐, si ottiene
๐ ๐ โค ๐ + ๐๐
2โค ๐ + ๐ + ๐
๐
4โค ๐ + ๐ + ๐ + ๐
๐
8= 3๐ + ๐
๐
23
In generale
๐ ๐ โค ๐๐ + ๐๐
2๐
Per ๐ = log2 ๐ si ottiene๐ ๐ โค ๐ โ log2 ๐ + ๐ 1 = ๐(log2 ๐)
Fib6 quindi รจ esponenzialmente piรน veloce di Fib3
Riepilogo: Complessitร e Confronto di Soluzioni Diverse
34
Algoritmo Complessitร in Tempo Complessitร in Memoria
Fib2 ๐(2๐) ๐(๐)
Fib3 ๐(๐) ๐(๐)
Fib4 ๐(๐) ๐(1)
Fib5 ๐(๐) ๐(1)
Fib6 ๐(log2 ๐) ๐(log2 ๐)