Sei sulla pagina 1di 9

24/04/2017

1 Costo computazionale degli algoritmi

Costo computazionale degli algoritmi

Premesse.
Utilizzare al meglio gli strumenti dell’Informatica significa anche conoscere e studiare il
comportamento degli algoritmi nella soluzione di problemi Parleremo quindi di soluzioni di
problemi con l’ausilio del calcolatore, parleremo di algoritmi e di come si possa stabilire la “bontà”
di un algoritmo, di come si possano confrontare fra loro algoritmi differenti per stimarne
l’efficienza Oggi esistono problemi che, anche con il più potente dei computer, richiederebbero
centinaia di anni per essere risolti Ci chiediamo se prima o poi si riuscirà a trovare per ogni
problema un algoritmo efficiente che possa risolverlo in tempi ragionevoli, o se questo obiettivo è
impossibile
A fronte di un certo problema possono essere proposti algoritmi differenti per risolverlo: come
faccio a stabilire quale è il migliore? L’algoritmo migliore è il più efficiente: quello che riesce a
risolvere il problema con il minor utilizzo di risorse (della macchina) e nel minor tempo possibile
Uno stesso algoritmo, eseguito su macchine diverse, impiega tempi diversi! Allora l’efficienza di
un algoritmo è legata alla potenza della macchina su cui lo eseguo? No: una buona misura
dell’efficienza deve prescindere dal calcolatore su cui eseguirò l’algoritmo, altrimenti invece di
misurare l’efficienza dell’algoritmo misurerei l’efficienza della macchina!
L’efficienza di un algoritmo è data dal numero di operazioni elementari compiute dall’algoritmo,
calcolate in funzione della “dimensione dell’input” , ossia in funzione del numero di dati che
dovranno essere elaborati

L’efficienza di un algoritmo non è dunque un numero, ma una funzione

Diventa interessante quindi parlare di costo computazionale di un algoritmo quando il tempo T


per la sua esecuzione dipende dalla mole n dei dati in ingresso nel seguente modo:

n → +∞ ⇒ T(n) → +∞

Non per tutti gli algoritmi è così: nell’algoritmo che calcola il massimo tra due numeri, in quello
che calcola l’area del trapezio, nell’algoritmo che esegue delle prove sul troncamento della
divisione tra interi, … i dati di ingresso sono una quantità fissa, non variano di dimensione e di
conseguenza il tempo di esecuzione dell’algoritmo è noto e finito.
Nel calcolo della somma o del massimo su n dati acquisiamo gli n valori dell’array: la dimensione
dei dati varia con n e di conseguenza variabile è il tempo di esecuzione dell’algoritmo.

n = 10 l’algoritmo esegue 10 somme


n = 10000 l’algoritmo esegue 10000 somme

Si chiama complessità computazionale la funzione F(n) che calcola il numero di operazioni


eseguite:

n: dimensione dei dati di ingresso


F(n) numero di operazioni
n → +∞ ⇒ F(n) → +∞
24/04/2017
2 Costo computazionale degli algoritmi

Ha interesse sapere come la funzione F(n) “cresce” al crescere di n


vale a dire quale è il suo ordine di infinito.

Notazione O (o-grande).
Sia f(n) la funzione di complessità che cerchiamo. Si dice che f(n) = O(g(n)) ovvero che g(n) è una
limitazione superiore per f(n) se:

f(n) è O(g(n)) se Come ordine di infinito f “cresce non più di” g.

∃ c, n0 > 0: ∀ n ≥ n0 Esempio. n2 + n è O(n2)


f(n) ≤ c * g(n) infatti: n2 + n ≤ n2 + n2 = 2n2
24/04/2017
3 Costo computazionale degli algoritmi

Notazione Ω (omega).
Sia f(n) la funzione di complessità che cerchiamo. Si dice che f(n) = Ω(g(n)) ovvero che g(n) è una
limitazione inferiore per f(n) se:

f(n) è Ω(g(n)) se Come ordine di infinito f “cresce almeno quanto” g.

∃ c, n0 > 0: ∀ n ≥ n0 Esempio. n2 + n è Ω(n)


f(n)  c * g(n) infatti: n + n  n + n = 2n
2

Notazione Θ (theta).
Sia f(n) la funzione di complessità che cerchiamo. Si dice che

f(n) è Θ(g(n)) se Come ordine di infinito f “cresce quanto” g.

∃ c1, c2, n0 > 0: ∀ n ≥ n0 Esempio. n2 , 1000 n2, 1/100 n2 sono tutte Θ(n2)
c1 g(n) ≤ f(n) ≤ c2 g(n) infatti: varia solo la costante
moltiplicativa
24/04/2017
4 Costo computazionale degli algoritmi

Funzioni di riferimento
Consideriamo delle funzioni di riferimento
e calcoliamo la complessità degli algoritmi
confrontandola con queste funzioni.

y = log x
y=x
y = x log x
y = x2
y = 2x
y = ex

Classi di costo
Quando scriviamo un algoritmo, vogliamo stimare la sua complessità cercando di individuare la
funzione g(n) che approssima f(n). In tale modo gli algoritmi vengono suddivisi in classi di
complessità:

costanti k (non dipendono da n)


logaritmo log n , log2n
lineare n
n log n
polinomiale nk k = 2, 3, …
esponenziale n! , a , nn a ≠ 0, 1
n

Ordini di grandezza:

Avendo a disposizione un elaboratore che esegue 103 operazioni al secondo, l’algoritmo risolutivo
di un problema 2n (ultima colonna) con ingresso di dimensione:

n=10 impiega 1 sec


n=20 impiega 1000 sec (17 min)
n=30 impiega 106 sec (>10 giorni)
n=40 impiega 109 sec (>>10 anni)

Quando si scrive un algoritmo si deve sempre dare una stima della limitazione superiore. È
opportuno dare una stima della limitazione inferiore e sarebbe importante stimare anche un
comportamento medio tra le due stime, ma questo spesso è difficile.
O(g) limitazione superiore: sono richieste al più g(n) operazioni
Ω(g) limitazione inferiore: sono richieste almeno g(n) operazioni.
24/04/2017
5 Costo computazionale degli algoritmi

Problemi algoritmici "difficili"


Vi sono problemi algoritmici i cui unici algoritmi risolventi sono esponenziali, e per i quali si è
dimostrato che non possono esistere algoritmi migliori; esempi: le torri di Hanoi, il problema del
commesso viaggiatore, il calcolo dei cammini da un angolo a quello opposto di una griglia,
l’allineamento di sequenze di DNA. I migliori algoritmi nella crittografia hanno una complessità
(√ln(ln(𝑛)))
dell’ordine di: O(𝑒 ).
Si tratta in generale di problemi per i quali anche solo verificare che la soluzione è corretta
richiede un tempo esponenziale.

Problemi algoritmici non risolubili


Non tutti i problemi algoritmici sono risolubili: per alcuni problemi algoritmici si può dimostrare
che non può esistere una soluzione, cioè che non può esistere un algoritmo che risolve il
problema. Esempi:
 Dati due programmi java arbitrari, determinare se essi sono equivalenti, cioè se per
qualunque input producono lo stesso output.
 Dato un programma java arbitrario, ed un input legale arbitrario per tale
programma, determinare se il programma, per quell’input, termina.

Il problema dell’arresto
È legittimo considerare algoritmi che indagano sulle proprietà di altri algoritmi, che sono trattati
come dati. Infatti gli algoritmi sono rappresentabili con sequenze di simboli, che possono essere
presi dallo stesso alfabeto usato per codificare i dati di input. Una stessa sequenza di simboli può
essere quindi interpretata sia come un programma, sia come un dato di ingresso di un altro
programma.
Il problema dell’arresto consiste nel chiedersi se un generico programma termina la sua
esecuzione, oppure va in ciclo, ovvero continua a ripetere la stessa sequenza di istruzioni
all’infinito (supponendo di non avere limiti di tempo e memoria). Il problema dell’arresto è
INDECIDIBILE.
L’algoritmo ARRESTO costituirebbe uno strumento estremamente potente: permetterebbe infatti
di dimostrare congetture ancora aperte sugli interi (esempio: la congettura di Goldbach).

Congettura di Goldbach. XVIII secolo


“ogni numero intero pari n ≥ 4 è la somma di due numeri primi”
Congettura falsa Goldbach() si arresta
Congettura vera Goldbach() NON si arresta

Goldbach() {
n = 2;
do { n = n + 2;
controesempio = true;
for (p = 2; p ≤ n -2; p++) {
q = n – p;
if (Primo(p) && Primo(q)) controesempio = false;
}
} while (!controesempio);
return n;
}
24/04/2017
6 Costo computazionale degli algoritmi

Costo computazionale delle operazioni


Quali sono le operazioni che un algoritmo esegue?

 operazioni elementari
 assegnazioni (accesso e assegnamento)
 confronti
 lettura/scrittura
 invocazione di funzioni (passaggio parametri e gruppi di istruzioni all’interno della
funzione)
 strutture di controllo

Assegnazione.
1) a = 25; Indichiamo con
2) a = sqrt(3*sin(x) + cos(y)/7);
ta
Queste due assegnazioni necessitano di due tempi diversi e
il tempo per una assegnazione.
sicuramente sarà: t1 < t2 perché l’espressione 2) richiede più calcoli.

Sequenza di assegnazioni.
<a1>
<a2> k costante Sommiamo i tempi:
…. il numero di istruzioni
k ta ~ ta
<ak> non dipende da n
k non dipende da n.
Se abbiamo due funzioni F1(n) F2(n) con lo stesso ordine di infinito,
può essere importante stimare anche il valore di k.

Struttura condizionale.
se P
allora <a1> <a1> e <a2> rappresentano
altrimenti <a2> gruppi di istruzioni
Se <a1> e <a2> non dipendono da n:
finese
tp + ta1  tp + ta2  tp
Il predicato P potrà essere:
abbiamo un numero costante di
1) a<b operazioni, come nell’algoritmo del
max(a,b).
2) (a<b) o (non S) e ((a==c) o (c!=h))

Certamente si avrà t1 < t2 ma consideriamo tp il tempo per valutare un predicato. Avremo

tp + ta1 oppure
tp + ta2
24/04/2017
7 Costo computazionale degli algoritmi

Struttura iterativa.
tp: eseguito n+1 volte
Consideriamo un ciclo con incremento fisso e passo 1:
(n volte con P vero, 1 volta con P falso)
ti: eseguito n volte
per i da 1 a n eseguire
iterazione //indipendente da n (n+1)·tp + n * ta ~ n * t
fineper
t = costante: “costo” dell'iterazione
Indichiamo con tp il tempo per il predicato e con ti il tempo
per l'iterazione (per ora indipendente da n): Avremo:

(n+1)·tp + n·ti = n·tp + tp + n·ti ≤ n·tp + n·tp + n·ti = n·( 2·tp + ti) = n * t (n ≥ 1)

Struttura iterativa nidificata.


Cosa cambia se anche l’iterazione dipende da n? Supponiamo che l’iterazione sia un ciclo dello
stesso tipo, che viene eseguito n volte:

1. per i da 1 a n eseguire
2. per k da 1 a n eseguire
2
iterazione n t
fineper
fineper

(n+1)·tp1 + ((n+1) tp2 + n·ti) * n =


= (n+1)·tp1 + ((n+1) tp2)·n + n2·ti ≤ 2n· tp1 + 2n·tp2·n + n2·ti ≤ n2 (2 tp1+ 2 tp2 + ti) = n2 * t

avendo considerato: n+1 ≤ 2n , 2n ≤ 2n2 con n naturale (positivo)


t = costante : “costo” dell'iterazione
24/04/2017
8 Costo computazionale degli algoritmi

Esempi di calcolo della complessità di un algoritmo

Esempio 1
A B B
algoritmo 1 n+1 n
RicercaSequenziale( a , k ) {
trovato = false; ta
indice = -1; ta
i = 0; ta
while ((i<n) && (!trovato)) { tp
if (a[i] == k) { tp
trovato = true; ta
indice = i; ta
}
else i = i+1; ta
}
return indice; ta
}

Riprendendo i calcoli visti in Struttura iterativa nidificata (log n  log2 n):


_A_ ___B___ ____C_____
4ta + (n+1)*tp + n*(tp + 3ta) < 4ta*n + 2n*tp + n*(tp + 3ta) < n*(3ta + 2*tp + tp + 3ta) = c*n

f(n) < c * n  f(n) = O(n)

Esempio 2
A B C D
algoritmo n-2+1 n-2 Log n +1 log n
(1) for (i=2; i<n; i++) { tp
(2) j=i; ta
(3) printf("%d", j); ta
(4) while (!(j%2)) { tp
(5) j/=2; ta
(6) printf("%d", j); ta
(7) }
}

Riprendendo i calcoli visti in Struttura iterativa nidificata (log n  log2 n):

___A___ ______B___________________________
____C_____ + ____D______
(n-1)*tp + (n-2)*(2ta + tp (log n + 1) + (log n) (2ta)) < n*tp + n*2ta +n*tp* log n + n*tp +
n*log n * 2ta < c * n*log n

f(n) < c * n log n  f(n) = O(n log n)


24/04/2017
9 Costo computazionale degli algoritmi

Esempio 3
algoritmo 1 n/2+1 n/2 log n log n
private static void q( int[] v ) {
int n = v.length - 1; ta
for ( int k=n/2; k>0; k=k-1 ) { tp1
int i = k; ta
int j = 2 * i; ta
do {
if ((j < n) && (v[j] < v[j+1])){
j = j + 1; tp
} ta
if ( v[i] < v[j] ) {
int x = v[i]; tp
v[i] = v[j]; ta
v[j] = x; ta
} else { ta
break;
} ta
i = j;
j = 2 * i; ta
} while ( j <= n ); tp2
}
}
In sostanza la dipendenza da n riguarda due cicli come segue:

1. for : n/2 +1: k viaggia da n/2 a 1 compresi


2. do {} while: il ciclo ha un numero variabile di iterazioni in funzione dei risultati degli if. Il
caso peggiore (maggior numero di iterazioni) si ha per k= 1 e quindi i=1 e j=2. Ragionando
sempre sul caso peggiore (gli if falliscono): per k=1 si inzia con i=j, j=2i ovvero i=2 e j= 4.
Nei giri successivi i=4 e j=8, i= 8 e j = 16 … fino al tetto di n. La progressione di j è
esponenziale quindi il numero di iterazioni è logaritmico: log n!!

tempo di esecuzioni di un ciclo while: tw = 2tp + 7ta (non dipendono da n)


numero esecuzioni ciclo while: log n * tw
numero esecuzioni controllo while: log n * tp2
tempo di esecuzioni di un ciclo for: tf = 2ta + while = 2ta + log n * (tp2 + tw)
numero di esecuzioni ciclo for: n/2 * tf = n/2 * ( 2ta + log n * (tp2 + tw))
numero di esecuzioni controllo “for”: (n/2+1) tp1

sommando le cinque componenti:

(n/2+1) tp1 + n/2 * (2ta + log n * (tp2 + tw)) < [perché n/2+1 < n]
n * tp1 + n * ta + n * log n * tp2 + n * log n * tw =
n * (tp1 + ta) + n * log n * (tp2 + tw) < n log n (tp1 + ta + tp2 + tw ) [perché n < n log n]

f(n) < c * n log n  f(n) = O(n log n)

Potrebbero piacerti anche