Comportamento asintotico: notazione O
Istruzione dominante
Comportamento asintotico: notazione W

Cenni sulla Complessità degli Algoritmi

Delimitazioni alla complessità di un problema
Classi di complessità

"La complessità computazionale si occupa della valutazione del costo degli algoritmi in termini di risorse di calcolo, quali il tempo di elaborazione e la quantità di memoria utilizzata. Nel contesto della complessità computazionale vengono anche studiati i costi intrinseci alla soluzione dei problemi, con l'obiettivo di comprendere le prestazione massime raggiungibili da un algoritmo applicato a un problema." (Cadoli)

La Teoria della complessità, che ha avuto un forte sviluppo intorno agli anni '70 si occupava appunto di:

- complessità di un problema;
- valutazione dell'efficienza degli algoritmi.

Un programma richiede spazio di memoria e tempo di calcolo.
Ci concentriamo su quest'ultimo per valutare la complessità dei programmi.

Esempio: C = A x B

Moltiplicazione di due matrici quadrate nxn di interi: per calcolare C[i,j] si eseguono 2n letture, n moltiplicazioni, n-1 addizioni ed 1 scrittura.

Per calcolare C: n2*(2n letture, n moltiplicazioni, n-1 addizioni ed 1 scrittura):

time(n) = 2n3+n3+n2*(n-1) +n2 = 4*n3
Dato un algoritmo A la complessità viene determinata contando il numero di operazioni aritmetiche e logiche, accesso ai file, letture e scritture in memoria, etc.

Occorre tener presente che il tempo t dipende dai seguenti fattori:

Ipotesi semplificativa: tempo impiegato sia proporzionale al numero di operazioni eseguite (ciascuna a costo unitario), non ci si riferisce, quindi, ad una specifica macchina, ma ci si bassa su un modello astratto di calcolatore che non prenda in considerazione la peculiarità di una singola macchina. Occorre inoltre svincolarsi da una particolare configurazione dei dati in ingresso, ad esempio basandosi sulla configurazione che rappresenta il caso peggiore, ovvero quello che comporta il maggior costo computazionale.
Il tempo impiegato per risolvere un problema dipende sia dall'algoritmo utilizzato che dalla "dimensione" dei dati a cui si applica l'algoritmo. Quindi nell'ipotesi semplificativa, il tempo deve essere una funzione della dimensione dell'input, in modo da svincole l'analisi dalla quantità di dati in ingresso considerati in una esecuzione.

Valutare la complessità degli algoritmi ci consente di scegliere tra loro quello più efficiente (a minor complessità).


La complessità dell'algoritmo viene espressa in funzione della dimensione delle strutture dati.

Esempio: algoritmo del generico problema P

è molto diverso da perché cambia l'ordine di grandezza del problema.

Esempi:


n log2 n n*log2 n n2 n3 2n
2 1 2 4 8 4
10 3,322 33,22 102 103 > 103
102 6,644 664,4 104 106 >> 1025
103 9,966 9996,0 106 109 >> 10250
104 13,287 1328,7 108 1012 >> 102500
NB: la scelta della base 2 per logaritmi ed esponenziali è dettata solo da ragioni di comodità: nulla cambierebbe usando basi diverse (loga n differisce da logb n per un fattore costante loga b, e lo stesso vale per an rispetto a bn)

Per esempio, avendo a disposizione un elaboratore che esegue 103 operazioni al secondo, l'algoritmo risolutivo del problema P con ingresso di dimensione 10 impiega 1 sec., con ingresso di dimensione 20 impiega 1000 sec (17 min), con 30 106 sec (>10giorni), con 40 109 sec (>>10 anni)
 
 

Comportamento asintotico: notazione O      back


Individuare con esattezza la funzione time(n) è spesso molto difficile.

Spesso, però, è sufficiente stabilire il comportamento asintotico della funzione quando le dimensioni dell'ingresso tendono ad infinito (comportamento asintotico dell'algoritmo).
 

Definizione


Un algoritmo (programma) ha costo O(f(n)) (o complessità O(f(n)) ) se esistono opportune costanti a,b, n' tali che:

timeA(n) < a*f(n)+b per ogni n>n'.
O(f(n)) si dice limite superiore al comportamento asintotico di una funzione.

Esempi:

Attraverso la notazione O( ), gli algoritmi vengono divisi in classi, ponendo nella medesima classe tutti quelli la cui complessità è dello stesso ordine di grandezza.

Si hanno così le suguenti:
 
 
 

Classi di complessità     back


1) Complessità costante
O(1)
È posseduta dagli algoritmi che eseguono sempre lo stesso numero di operazioni indipendentemente dalla dimensione dei dati.
Es.: inserimento o estrazione dalla testa di una lista concatenata, ecc.

2) Complessità sottolineare
O(n ), con k<1 k
Es.:Ricerca binaria, o logaritmica, ecc.

3) Complessità lineare
O(n)
È posseduta dagli algoritmi che eseguono un numero di operazioni sostanzialmente proporzionale ad n. Es.: ricerca sequenziale, somma di numeri di n cifre, merge di due file, ecc.

4) Complessità n log n
O(n log n)
Es.: Algoritmi di ordinamento ottimi.

5) Complessità n^k , con k>=2
O(n^k)
Es.: Bubblesort ( O(n^2) ), moltiplicazione di due 2 matrici ( O(n^3) ), ecc.

6) Complessità esponenziale
Tutte le classi di complessità elencate precedentemente vengono genericamente considerate come polinomiali. Esse sono caratterizzate dal fatto che la dimensione n non compare mai come esponente in alcun modo. Quando ciò avviene si parla invece di complessità esponenziale.
Es.: un algoritmo che debba produrre tutte le possibili stringhe lunghe n di k simboli avrà complessità O(k^n) cioè esponenziale.
L'algoritmo della Torre di Hanoi ha complessità O(2^n) (sono necessarie 2^n - 1 mosse).
È necessario guardare con particolare diffidenza agli algoritmi dotati di complessità esponenziale, perché possono richiedere dei tempi di esecuzione proibitivi (se non addirittura astronomici) anche per valori relativamente piccoli di n ed indipendentemente dalla velocità dell'elaboratore.
Purtroppo esistono molti problemi, anche di interesse pratico, per i quali non si conoscono ancora algoritmi non esponenziali (problemi intrattabili). Es.: problema del commesso viaggiatore.
Un problema è computazionalmente trattabile se esiste un algoritmo efficiente che lo risolve; altrimenti il problema è computazionalmente intrattabile.
Un algoritmo si dice efficiente se il suo tempo di esecuzione è limitato superiormente da una funzione
polinomiale (funzione limitata superiormente da n^k per un opportuno fissato intero k).

Esempi:


Dato un problema P e due algoritmi A1 e A2 che lo risolvono siamo interessati a determinare quale ha complessità minore (è "il migliore" dal punto di vista dell'efficienza computazionale).

Esempio:

timeA1(n)=3n2+n

timeA2(n)=n2+10n

Per n>=5, timeA2(n)<timeA1(n). La complessità di A2 è minore per ingressi con dimensione maggiore di 5.

NB: non è detto che quel che vale per n "grande" valga anche per n "piccolo". Ma ci interessa che un algoritmo sia efficiente per n grande (se n "piccolo", c'è poca differenza, e comunque ogni algoritmo va bene... o quasi!)
 

Definizione:


Dato un problema P e due algoritmi A e B che lo risolvono con complessità timeA e timeB, diciamo che A è migliore di B nel risolvere P se:

(1) timeA=O(timeB)

(2) timeB non è O(timeA)
 

Nell'esempio sopra, timeA1=O(timeA2) e viceversa: A1 e A2 hanno entrambi comportamento asintotico quadratico (O(n2)). Sono dunque equivalenti dal punto di vista del costo computazionale.

Controesempio:

timeB1(n)=3n2+n

timeB2(n)=n*log n
 
 

Comportamento asintotico: notazione W    back

Definizione

Un algoritmo (programma) ha costo W(g(n)) (o complessità W(g(n)) ) se esiste una opportuna costante positiva c tale che:

timeA(n) > c*g(n)

per un numero infinito di valori di n.

Esempio

3n2+n = W(n2) (ma anche W(n) e W(n*log n))

W(g(n)), rappresenta un limite inferioreal comportamento di una funzione.

Si ha una valutazione esatta del costo di un algoritmo quando le due delimitazioni O(f(n)) e W(f(n)) coincidono.
 
 

Delimitazioni alla complessità di un problema    back

Finora ci siamo interessati della complessità del singolo algoritmo che risolve un certo problema.
Abbiamo visto che possono esistere algoritmi più o meno complessi per risolvere lo stesso problema (e ovviamente preferiremo quelli meno complessi).
Perciò è interessante capire se il problema in quanto tale abbia una sua complessità, cioè se sia "intrinsecamente facile" o "intrinsecamente difficile" - indipendentemente dall'algoritmo che possiamo inventare per risolverlo.
Diremo allora che:

Un problema ha delimitazione superiore O(f(n)) alla sua complessità se esiste almeno un algoritmo di soluzione per il problema con complessità O(f(n)).

Un problema ha delimitazione inferiore W(g(n)) alla sua complessità se è possibile dimostrare che ogni algoritmo di soluzione per il problema ha complessità almeno W(g(n)).

Note:

Un algoritmo di soluzione di un problema P è ottimale quando l'algoritmo ha complessità O(f(n)) e la delimitazione inferiore alla complessità del problema è W(f(n)).

Infatti, se la complessità del problema è W(f(n)), significa che meno di f(n) non si potrà mai fare; e quindi i migliori algoritmi avranno appunto complessità O(f(n)) [algoritmi peggiori potrebbero avere complessità superiore, ma ovviamente cercheremo di evitarli].

In particolare diremo che un problema ha complessità:

Problema intrattabile: è un problema risolubile, ma per il quale non esiste alcun algoritmo con complessità polinomiale che lo risolve (ad esempio, problema del commesso viaggiatore).
 
 

Istruzione dominante     back


Permette di semplificare in modo drastico la valutazione della complessità.
 

Definizione


Dato un algoritmo (o programma) A il cui costo di esecuzione è t(n), un'istruzione di A è detta dominante quando, per ogni intero n, essa viene eseguita (nel caso di input con dimensione n) un numero d(n) di volte tale che:

t(n) < a d(n) + b

per opportune costanti a, b.

In pratica, una istruzione dominante viene eseguita un numero di volte proporzionale al costo dell'algoritmo.

Perciò, se esiste un'istruzione dominante, la complessità dell'algoritmo si riconduce a quella legata all'istruzione dominante, ossia la complessità dell'algoritmo è O(d(n)).

Esempio: ricerca esaustiva di un elemento in un vettore di dimensione N

boolean ricerca (vettore vet, int el){
int i=0; boolean T=falso;

while (i<N) { /* (1) */
if (el==vet[i]) /* (2) */ T=vero;
i++;
}

return T;

}

Le istruzioni (1) e (2) sono quelle dominanti, e vengono eseguite rispettivamente N+1 volte (per i=0..N) e N volte (per i=0..N-1): dunque il costo è lineare.


Esempi su vettori: Ricerca del valore minimo e massimo di un vettore

int minimo (vettore vet) {
int i, min;

for (min = vet[0], i = 1; i < N; i ++)
if (vet[i]<min) /* istr. dominante */
min = vet[i];

return min;
}
 

int massimo (vettore vet) {
int i, max;

for (max = vet[0], i = 1; i < N; i ++)
if (vet[i]>max) /* istr. dominante*/
max=vet[i];

return max;
}

Sia per la ricerca del minimo che del massimo, l'istruzione dominante viene eseguita N-1 volte.

Perciò entrambi gli algoritmi hanno un costo O(n), se n è la dimensione del vettore.
 
 

Dipendenze dai dati d'ingresso     back


Molto spesso il costo dell'esecuzione di un algoritmo dipende non solo dalla dimensione dell'ingresso, ma anche dai particolari valori dei dati in ingresso.

E' il caso tipico di molti algoritmi di ordinamento: se il vettore è già ordinato (o quasi), finiscono subito, mentre se è "molto disordinato" devono eseguire molti più passi.

Si distinguono diversi casi: caso migliore, caso peggiore, caso medio.

Di solito la complessità viene valutata nel caso peggiore (e talvolta nel caso medio).
 

Esempio: Per la ricerca sequenziale in un vettore il costo dipende dalla posizione dell'elemento cercato.

Caso migliore: l'elemento è il primo del vettore (un solo confronto)

Caso peggiore: l'elemento è l'ultimo o non è presente: l'istruzione dominante è eseguita N volte (N dimensione del vettore). Il costo è lineare.

OSSERVAZIONE.
Il caso peggiore è utile, ma non sempre significativo in pratica, perché solitamente è anche raro. Ha quindi senso cercare di capire cosa succede "di solito", in un caso "medio".

Caso medio: ciascun elemento sia equiprobabile

L'elemento cercato potrebbe essere il primo ( 1 confronto), o il secondo (2 confronti), ..., oppure l'ultimo ( N confronti)

time(N) = S(i=1..N) Prob(el(i)) * i = S(i=1..N) (1/N) * i = (N+1)/2

 

Informazioni e complessità     back


Il risultato precedente dipende dal fatto che non avevamo particolari informazioni da sfruttare per "guidare" la ricerca.

Invece, sapendo che il vettore è ordinato, la ricerca può essere ottimizzata.

Esempio: Ricerca binaria

Rispetto alla ricerca esaustiva, la tecnica di ricerca binaria consente a ogni passo di dimezzare il vettore.


 

Calcolo della complessità della ricerca binaria     back


Come per la ricerca sequenziale, il costo dipende dalla posizione dell'elemento cercato.

Però, sarà più semplice trovarlo!

Caso migliore: l'elemento cercato è il primo con cui ci confrontiamo nel vettore (quello mediano): dunque, si fa un solo confronto

Caso peggiore: l'elemento cercato non è nel vettore.

Passo
n. elementi
n. confronti
1
N
1
2
N/2
1
3
N/4
1
...
   
k
N/2(k-1)
1
Perciò, poiché k è tale che 2(k-1) >= N,
time(N) = S(i=1..k) 1 = k = (log2 N)



back