Algoritmo: sequenza finita di passi, elementari e non ambigui, che

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): NN,
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 (yL) 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