Tempo e spazio di calcolo Modelli di calcolo e metodologie di analisi F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) In quale modo stimiamo il tempo di calcolo? Possiamo considerare due approcci: • Approccio empirico (a posteriori) • Approccio teorico (a priori) SECONDO VOI QUALE STUDIEREMO IN QUESTO CORSO ? SECONDO VOI QUALE E’ IL PIU’ UTILE IN PRATICA ? F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Sviluppiamo una metodologia per l’approccio teorico Per sviluppare la metodologia per stimare il tempo e lo spazio di calcolo dobbiamo precisare: 1. 2. 3. 4. Linguaggio per descrivere gli algoritmi Modello computazionale d’esecuzione Metrica per misurare il tempo di calcolo Modo per caratterizzare il tempo di calcolo per gli algoritmi ricorsivi F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) 1. Linguaggio per descrivere gli algoritmi Pseudo-codice Java-like: assegnazione: ← i ← j ← k equivale alla seq.: j ← k; i ← j espressioni: simboli matematici standard per espressioni numeriche e booleane commento: { } dichiarazione di metodo: nome (param 1, param 2, ...) chiamata di un metodo: nome (param 1, param 2, ...) ritorno da un metodo: return valore F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Pseudo-codice Java-like (continua): • dati composti: • i-esimo elemento array A: A[i] • A[i . . j] ≡ <A[i], A[i+1], . . . , A[j]>, se i ≤ j sequenza vuota, se i > j • i dati composti sono organizzati in oggetti, che sono strutturati in attributi o campi: ad es. length[A] • una variabile che rappresenta un oggetto è un riferimento • un riferimento che non si riferisce a nessun oggetto: nil • parametri alle procedure passati per valore (per gli oggetti una copia del riferimento) F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Pseudo-codice Java-like (continua): • struttura di blocco: spaziatura • costrutti iterativi e condizionali : if condizione then azioni (else azioni) while condizione do azioni do azioni while condizione for variabile ← val-iniz. to val-fin. (incremento) do azioni for variabile ← val-iniz. downto val-fin. (decremento) do F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) azioni 2. Il modello computazionale d’esecuzione Consideriamo come operazioni primitive le seguenti operazioni : 1. 2. 3. 4. 5. 6. 7. assegnazione di un valore ad una variabile chiamata di un metodo eseguire un’operazione aritmetica confronto di due numeri indicizzazione di un elemento in un array riferimento a un oggetto rientro da un metodo F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Assunzione implicita: il numero di operazioni primitive è proporzionale al tempo di esecuzione dell’algoritmo • Questo approccio dà origine al modello computazionale chiamato Random Access Machine (RAM): • CPU connessa a un banco di celle di memoria • ogni cella memorizza una parola (un numero, un carattere, . . . In generale: il valore di un tipo di base o un riferimento ad un oggetto) • la CPU accede ad una cella di memoria arbitraria con una operazione primitiva (ogni operando e’ una cella di memoria) F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) La quantità di tempo (e di spazio) consumato dall’esecuzione di un programma RAM su un dato input può essere determinato essenzialmente usando due criteri: Criterio di costo uniforme: l’esecuzione di un’istruzione primitiva richiede un tempo indipendente dalla “grandezza” degli operandi (ricordate: un operando e’ o un valori di un tipo base o un riferimento ad un oggetto) Criterio di costo logaritmico: il tempo di calcolo richiesto da un’istruzione primitiva dipende dal numero di bit necessari a rappresentare gli operandi F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) 3. Metrica per misurare il tempo di calcolo Misureremo il tempo di calcolo contando le operazioni primitive Ad esempio: Massimo (A, n) current-max ← A[0] for i ← 1 to n-1 do if current-max < A[i] then current-max ← A[i] return current-max 2 1+n 4*(n-1)…6 *(n-1) 1 Numero di operazioni primitive: t (n) = minimo 2 + 1 + n + 4*(n-1) + 1 = 5*n massimo 2 + 1 + n + 6*(n-1) + 1 = 7*n - 2 F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) In generale, istanze diverse avranno tempi di calcolo diversi, ad esempio: tempo nel caso peggiore 5 ms tempo di calcolo tempo nel caso migliore 3 ms 1 ms A B C D E F G istanze in input F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Analisi del caso medio Il tempo di calcolo medio dell’algoritmo definito come media dei tempi per tutti i possibili input Per poterlo calcolare si deve conoscere la distribuzione di probabilità sull’insieme dei possibili input In generale si tratta di un compito non banale. In mancanza di informazioni si puo’ assumere una distribuzione uniforme, ma si tratta di un’assunzione arbitraria! F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Analisi del caso peggiore • non richiede teoria probabilità • caso peggiore quasi sempre facile da identificare • se un algoritmo si “comporta bene” nel caso peggiore, si “comporta bene” su ogni input (fornisce una garanzia!) Analisi del caso ottimo • non richiede teoria probabilità • caso ottimo quasi sempre facile da identificare • se un algoritmo si comporta male nel caso ottimo, si comporta male su ogni input F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) 4. Modo per caratterizzare il tempo di calcolo degli algoritmi ricorsivi Equazione di ricorrenza: una funzione che esprime il numero di operazioni sull’input di dimensione n in funzione del numero di operazioni su input di dimensione inferiore. Ad esempio: { n > 1} Massimo-ricorsivo (A, n) if n = 1 then return A[0] return max (Massimo-ricorsivo (A, n-1), A[n-1]) T(n) = 3 T(n-1) + k se n = 1 altrimenti T(n) = k*(n-1) + 3 F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) ESEMPIO: tempo di calcolo della moltiplicazione per somme successive Moltiplicazione per “somme successive” y x2 45 44 43 .. . 4 3 2 1 19 19 19 .. . 19 19 19 19 moltiplicazione (x1, x2) y ← x1 prod ← 0 while y > 0 do prod ← prod + x2 y ←y-1 return prod 855 F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) ESEMPIO: tempo di calcolo della moltiplicazione alla russa Moltiplicazione “alla russa” y1 45 22 11 5 2 y2 19 38 76 152 304 1 608 19 --76 152 --608 855 molt-russa (x1, x2) y1 ← x1 y2 ← x2 prod ← 0 while y1 > 0 do if y1 is odd then prod ← prod + y2 y1 ← y1 div 2 y2 ← y2 + y2 return prod F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Moltiplicazione per somme successive vs moltiplicazione alla russa moltiplicazione (x1, x2) y ← x1 prod ← 0 while y > 0 do prod ← prod + x2 y ←y-1 return prod nel while molt-russa (x1, x2) y1 ← x1 y2 ← x2 prod ← 0 while y1 > 0 do if y1 is odd then prod ← prod + y2 y1 ← y1 div 2 y2 ← y2 + y2 return prod moltiplicazione molt-russa 2 * x1 assegnazioni x1 somme x1 decrementi x1+1 confronti 3 * lg x1 assegnazioni lg x1 divisioni 2 * lg x1 somme 2 * lg x1 + 1 confronti 5 * x1+1 + 3 operazioni 8 lg x1+1 + 4 operazioni F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) f(m) = 5m + 4 g(m) = 8 lg m + 5 60 54 num ero di operazioni 50 49 44 40 39 34 29 30 27,46 24 19 20 29 17,68 14 13 9 10 31,58 25,68 23,58 21 30,36 5 4 0 0 1 2 3 4 5 6 7 8 9 10 moltiplicatore F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) 11 LA NOTAZIONE ASINTOTICA Introduciamo un’ulteriore astrazione : tasso di crescita o ordine di grandezza del tempo di calcolo • ogni passo nello pseudo-codice (e ogni statement in un linguaggio ad alto livello) corrisponde a un piccolo numero di operazioni primitive che non dipendono dalla dimensione dell’input • basta considerare il termine principale perchè i termini di ordine inferiore non sono significativi per n grande. L’ordine di grandezza del tempo di calcolo fornisce una semplice caratterizzazione dell’efficienza e consente di confrontare algoritmi alternativi. F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Efficienza asintotica degli algoritmi: come cresce il tempo di esecuzione con il crescere al limite della dimensione delle istanze in input Notazione asintotica Consideriamo funzioni dai naturali ai numeri reali non negativi F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Notazione O: O (g(n)) e’ l’insieme di tutte le funzioni f(n) per cui esistono due costanti positive c ed n0 tali che f(n) ≤ c • g(n) per tutti gli n ≥ n0 c g(n) f(n) tem po di run n0 n f(n) ∈ O (g(n)) F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Notazione Ω: Ω(g(n)) e’ l’insieme di tutte le funzioni f(n) per cui esistono due costanti positive c ed n0 tali che f(n) ≥ c • g(n) per tutti gli n ≥ n0 f(n) tem po di run c g(n) n0 n f(n) ∈ Ω (g(n)) F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Notazione Θ: Θ(g(n)) e’ l’insieme di tutte le funzioni f(n) per cui esistono tre costanti positive c1, c2 ed n0 tali che c1• g(n) ≤ f(n) ≤ c2 • g(n) per tutti gli n ≥ n0 c2 g(n) f(n) tem po di run c1 g(n) n0 n f(n) ∈ Θ (g(n)) F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Proprietà della notazione asintotica Transitiva: f(n) = Θ (g(n)) e g(n) = Θ (h(n)) f(n) = O (g(n)) e g(n) = O (h(n)) f(n) = Ω (g(n)) e g(n) = Ω (h(n)) Riflessiva: Simmetrica: f(n) = Θ (h(n)) f(n) = O (h(n)) f(n) = Ω (h(n)) f(n) = Θ (f(n)) f(n) = O (f(n)) f(n) = Ω (f(n)) f(n) = Θ (g(n)) Simmetrica trasposta: f(n) = O (g(n)) g(n) = Θ (f(n)) g(n) = Ω (f(n)) F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) • d(n) = O(f(n)) a . d(n) = O(f(n)), per ogni costante a > 0 • d(n) = O(f(n)) & e(n) = O(g(n)) d(n) + e(n) = O(f(n) + g(n)) • d(n) = O(f(n)) & e(n) = O(g(n)) d(n) . e(n) = O(f(n) . g(n)) • f(n) funzione polinomiale di grado d: f(n) = a0 + a1n + . . . + adnd f(n) = O(nd) F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Alcune classi di complessità O(log n) logaritmica O(n) lineare O(n2) quadratica O(nk) (k ≥ 1) polinomiale O(an) (a > 1) esponenziale trattabili non trattabili F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Alcune funzioni ordinate per velocità di crescita n log n n n n log n n2 nn32 2n 2 1 1,41 2 2 4 8 4 4 2 2 4 8 16 64 16 8 3 2,83 8 24 64 512 256 16 4 4 16 64 256 4.096 65.536 32 5 5,66 32 160 1.024 32.768 4.294.967.296 64 6 8 64 384 4.096 262.144 1,84 x 1019 128 7 11,31 128 896 16.384 2.097.152 3,40 x 1038 256 8 16 256 2.048 65.536 16.777.216 1,15 x 1077 512 9 22,63 512 4.608 262.144 134.217.728 1,34 x 10154 1.024 10 32 1.024 10.240 1.048.576 1.073.741.824 1,79 x 10308 F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) 10 10 9 9 8 8 9 8 8 8 7 7 6 6 5 4 4 3 1 5 4,75 4 2 logn radn n n logn n^2 n^3 2^n 2,81 3 2 1,73 1,41 1,58 1 2 2 2,32 2,58 2,24 2,45 2,65 3 3,17 3 2,83 3,58 3,32 3,46 3,16 3,32 3,46 0 2 3 4 5 6 7 8 9 10 11 12 F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) 60 logn radn n n logn n^2 n^3 2^n 50 40 30 20 10 0 2 3 4 5 6 7 8 9 10 11 12 F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) 4500 logn radn n n logn n^2 n^3 2^n 4000 3500 3000 2500 2000 1500 1000 500 0 2 3 4 5 6 7 8 9 10 11 12 F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) ESEMPIO: tempo di calcolo dell’algoritmo di ordinamento per inserzione Problema: ordinamento di numeri. Input: una sequenza di n numeri <a1, a2,…,an>. Output: una permutazione <a1’, a2’,…,an’> della sequenza di input tale che a1’≤ a2’ ≤ … ≤ an’. F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) 1 2 3 4 5 3 5 1 8 2 key = 5 1 2 3 4 5 3 5 1 8 2 3 5 5 8 2 key = 1 1 3 5 8 2 1 2 3 4 3 3 5 8 2 5 1 3 5 8 2 key = 8 1 2 3 4 5 1 3 5 8 2 key = 2 1 3 5 8 8 1 3 5 5 8 1 3 3 5 8 1 2 3 5 8 F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Insertion-sort (A) j←2 1 for j ← 2 to length[A] do 2 key ← A[j] 3 {inserisci A[j] nella sequenza A[1. .j-1] num. volte 1 n n–1 } NON E’ UN INVARIANTE! spostando a destra gli elementi > di A[j]} i←j–1 n–1 while i > 0 and A[i] > key do Σ tj ( j=2..n) A[i+1] ← A[i] Σ(tj – 1) ( j=2..n) i←i–1 Σ(tj – 1) ( j=2..n) A[i+1] ← key n–1 j ← j+1 n–1 T(n) = 1 + a*n + b*(n-1) + c*Σ tj + d*Σ(tj – 1) = = (a + b)*n + (1 – b) + c*Σ tj + d*Σ(tj – 1) 4 5 6 7 8 F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) T(n) = = = = 1 + a*n + b*(n – 1) + c*Σ tj + d*Σ(tj – 1) = (a + b) *n + (1 – b) + (c+d)* Σ tj – n + 1 = (a + b –1)*n + (2 – b) + (c+d)* Σ tj = e*n + f + g* Σ tj Caso migliore: A è ordinato T(n) è una funzione lineare di n. Caso peggiore: A è ordinato in ordine inverso T(n) è una funzione quadratica di n. Caso medio (considerando ogni permutazione è ugualmente probabile) T(n) è una funzione quadratica di n. F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) ESEMPIO: il problema della valutazione di un polinomio Input: una sequenza di n+1 numeri reali A = <a0, a1,…,an> e il valore della variabile x Output: il valore del polinomio di grado n: P(x) = a0 + a1x+ … + anxn Un algoritmo che risolve il problema: Poly-eval (A, x, n) 1 y←1 2 result ← A[0] 3 for i ← 1 to n do 4 y←y•x 5 result ← result +A[i] • y {y = xi} 6 return result L’algoritmo esegue 2*n moltiplicazioni n somme e 2*n assegnazioni. Ma si può fare meglio F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) La regola di Horner: P(x) = a0 + x (a1+ … + x (an-1+ x an))…) Un algoritmo basato sulla regola di Horner: Horner (A, x, n) 1 result ← A[n] 2 for i ← n - 1 downto 0 do 3 result ← result • x + A[i] 4 return result L’algoritmo esegue n somme, n moltiplicazioni e n assegnazioni. F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) Poly-eval (A, x, n) 1 y←1 2 result ← A[0] 3 for i ← 1 to n do 4 y←y•x 5 result ← result +A[i] • y 6 return result fuori dal for confronti nel for Poly-eval 5 n+1 8*n Horner (A, x, n) 1 result ← A[n] 2 for i ← n - 1 downto 0 do 3 result ← result • x + A[i] 4 return result Horner 4 n+1 6*n Poly-eval: 9*n + 6 Horner: 7*n + 5 F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) 1200 T(n) = 9 n + 6 1005 1000 825 800 735 782 712 645 600 642 555 572 465 502 375 400 285 195 200 0 T(n) = 7 n + 5 915 82 12 1 15 11 432 292 362 222 152 105 21 31 41 51 61 71 81 91 101 111 F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) L’algoritmo Horner è sicuramente migliore dell’algoritmo Poly-eval L’analisi asintotica non distingue però tra i due algoritmi: per entrambi si ottiene Θ(n) F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) RIEPILOGO • Una metodologia per l’approccio teorico alla stima del tempo di calcolo • Analisi dei casi medio, peggiore, ottimo • Efficienza asintotica degli algoritmi • Risposta ad alcune domande lasciate in sospeso durante le lezioni precedenti. • ESERCIZIO: Rispondete alle seguenti domande. Quali domande restano ancora in sospeso? Adesso abbiamo gli strumenti per rispondere a qualcuna di esse? Se si, quali sono le risposte? F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04) UN ALTRO ESERCIZIO • La correttezza degli algoritmi considerati nei lucidi precedenti (o di loro minime varianti) e’ stata dimostrata (nelle lezioni precedenti) con il metodo delle asserzioni. ANNOTATE (SCRIVENDO SULLA STAMPA DEI LUCIDI) IL CODICE CON LE ASSERZIONI CHE NE DIMOSTRANO LA CORRETTEZZA. In questo modo vi sara’ piu’ facile seguire il ragionamento sulla loro complessita’!!! F. Damiani - Alg. & Lab. 04/05 (da M. Zacchi - Alg. & Lab. 03/04)