Cenni alla complessità computazionale degli algoritmi. Notazione asintotica. Algoritmo: sequenza finita di passi, elementari e non ambigui, che risolve un problema in un tempo finito. Problema: consiste di un dominio contenente le infinite istanze del problema e di una domanda cui, per ogni istanza del problema, si può dare una risposta. Istanza: si ottiene ogni qualvolta si specifichi un preciso valore per ogni parametro del problema (assegnazione dei dati in ingresso). Esempio: Problema: Ricercare un elemento in un vettore. Dominio: L’insieme dei vettori e l’insieme degli elementi Istanza del problema: elemento 4, vettore [ 1, 2, 4, 6, 9]. Problema Decidibile Indecidibile – non esiste alcun algoritmo che la risolva Problemi decidibili Lo stesso problema può essere risolta da più algoritmi. Come valutare l’efficienza di essi? Dato algoritmo A che risolve il problema P si vuole determinare la complessità computazionale = quantità di risorse utilizzate da A per risolvere P. Risorse Spazio (occupaz. di memoria) Tempo di calcolo Spazio: secondario, basso costo Tempo di calcolo: Unità di misura? Per poter prescindere dal modello di computer: 1 unità di tempo= tempo richiesto per l’esecuzione di 1 operazione elementare (+,-,*, /, confronti). Data un istanza I di problema P, tempo di calcolo di algoritmo A sull’istanza I è il numero di operazioni elementari eseguite da A su I. Tempo di calcolo di A per P dipende dalla dimensione della istanza di P. Esempio: Il tempo di ricerca di un elemento in un vettore dipende dalla grandezza del vettore (quanti elementi contiene). Inoltre, tempo di calcolo dell’algoritmo può essere diverso su istanze della stessa dimensione. Esempio. Dobbiamo ricercare elemento 4, scandendo il vettore. Istanza 1: V1=[1,2,3,5,4] Istanza 2: V2=[4,5,1,2,3] 1 Anche se entrambi i vettore hanno 5 elementi, per la ricerca di “4” nel secondo caso basta controllare solo il primo elemento(il caso migliore tra quelli che potevano capitare), mentre nel primo caso, dobbiamo scandire intero vettore. Per valutare la complessita dell’algoritmo, si definisce la funzione complessità che assume un singlo valore per ogni dimensione considerando il caso peggiore (“worst case”): Complessità computazionale dell’algoritmo A su ogni istanza di dimensione n, denotata fA(n), è il massimo tempo di calcolo richiesto da A su tutte le istanze di dimensione n. Complessità dell’algoritmo A per P è il massimo numero di operazioni elementari necessarie per risolvere tutte le possibili (infinite) istanze di P di fissata dimensione. Analisi della funzione complessita computazionale di A: fA(n): NN, A, fA(n) è una funzione positiva e crescente. Determinante (per stabilire i limiti di applicazione di A) l’andamento di di fA(n) per n grande, ossia n ∞: analisi asintonica. Qui diventano trascurabili coefficienti moltiplicativi costanti addittive termini crescenti più lentamente dagli altri. Siamo cioè interessati al tasso di crescità di fA. La notazione asintotica: dati f(n), g(n) funzioni positive f(n) = O(g(n)) se c>0 tale che n≥n0 f(n)≤c*g(n) f(n) = Ω(g(n)) se c>0 tale che n≥n0 f(n) ≥c*g(n) f(n) = (g(n)) se c1, c2>0 tale che n≥n0 c1*g(n) ≤ f(n) ≤c2*g(n) Intuitivamente, per n sufficientemente grande (n≥n0) e per opportune costanti moltiplicative(c, c1, c2>0) si ha: se una funzione f(n) = O(g(n)), allora f(n) cresce al più come g(n) se una funzione f(n) = Ω(g(n)), allora f(n) cresce almeno come g(n) se una funzione f(n) = (g(n)), allora f(n) cresce esattamente come g(n) Esempio: f(n) = 2n2+5n+3 cresce esattamente come g(n)= n2. In questo caso f(n) = O(g(n)) = Ω(g(n)) = (g(n)). L’algoritmo A è polinomiale se fA(n)= O(nk) per qualche k>0 fissato. Esempio: Problema: ricercare un elemento in un vettore. Algoritmo: scandiamo il vettore, confrontando i suoi elementi con elemento da ricercare. Appena troviamo, restituiamo la posizione, altrimenti “-1”. n è numero di elementi in un vettore, fA(n)= O(n), quindi questo algoritmo è polinomiale. 2 Algoritmo “efficiente” = algoritmo polinomiale: O(nk) Algoritmo “inefficiente” = algoritmo non polinomiale = alg. Esponenziale: O(an) Esempio: ipotesi: 1 operazione = 10-6 secondi. funzione n n3 2n 3n 10 10-5sec 10-3sec 10-3sec 6*10-2sec Dimensione istanza 20 30 -5 2*10 sec 4*10-5 sec -3 8*10 sec 6.4*10-2 sec 1 sec 12.7 giorni 5 1ora 4*10 anni 40 6 *10-5sec 2.3 *10-1sec 36.6*103 anni 1.3 *1015 anni Un problema è polinomiale se esiste un algoritmo polinomiale che risolve P. Problema decidibile Problema trattabile = problema polinomiale Problema intrattabile = problema non polinomiale, ovvero per quale non esiste un algoritmo polinomiale che la risolve. Algoritmi di base. Esempio. L’elenco telefonico Ricerca per numero - ricerca sequenziale Ricerca per nome - ricerca in un insieme ordinato lessicograficamente, è più veloce. Problema1: Ricerca di un elemento in un vettore Algoritmo per la ricerca sequenziale (pseudocodice): algoritmo ricercaSequenziale(array L, elem x) booleano 1. for each (yL) do 2. if (y = x) then return trovato 3. return non_trovato Caso migliore: x si trova in prima posizione: Tbest(n)=1 Caso peggiore: x si trova in ultima posizione o non appartiene al vettore: Tworst(n)=n, dove n è la dimensione del vettore. 3 Problema2. Ricerca di un elemento in un vettore ordinato. Algoritmo iterativo per la ricerca binaria (pseudocodice): algoritmo ricercaBinariaIter(array L, elem x) booleano 1. a 1 2. b lunghezza di L 3. i (a+b)/2 4. while (L [i] x ) then do 5. if (L[i] > x) then b i-1 6. else a i+1 7. if ( a>b ) then return non_trovato 8. i (a+b)/2 9. return trovato Caso migliore: x si trova nella posizione centrale, cioè (1+n)/2 : Tbest(n)=1 Caso peggiore: x viene trovato all’ultimo confronto o non appartiene al vettore: Tworst(n)=O(log n). Dunque, la ricerca binaria è esponenzialmente più veloce della ricerca sequenziale. L’insieme dei dati in ingresso però deve essere ordinato. Algoritmo ricorsivo per la ricerca binaria (pseudocodice): algoritmo ricercaBinariaRic(array L, elem x) booleano 1. n lunghezza di L 2. if (n = 0) then return non_trovato 3. i n/2 4. else if ( L[i] = x ) then return trovato 5. else if ( L[i] > x ) then return ricercaBinariaRic( L[1;i-1] , x) 6. else if ( L[i] < x ) then return ricercaBinariaRic( L[i+1;n] , x) tempo= tempo all’interno della procedura + tempo speso per le chiamate ricorsive Tworst(n) = O(1) + T( (n-1)/2 ) Tecnica divide et impera: dividere un problema in sottoproblemi più piccoli, risolverli e combinare le loro soluzioni per ottenere la soluzione al problema originario. 4 Problema3. ordinare elementi di un vettore algoritmo bubbleSort(array L) 1. for i=1 to (n-1) 2. for j=2 to (n - i+1) 3. if ( L[j-1] > L[j] ) then scambia A[j-1] e a[j] 4. if (non ci sono stati scambiati) then break L’algoritmo di ordinamento a bolle opera seguendo una serie di scansioni dell’array: in ogni scansione sono confrontate coppie di elementi adiacenti, e viene effettuato uno scambio se i due elementi non rispettano ordinamento. Se durante una scansione non viene effettuato nessun scambio, l’array è ordinato e l’algoritmo termina. Tworst(n)=(n2) 5