






Studia grazie alle numerose risorse presenti su Docsity
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Prepara i tuoi esami
Studia grazie alle numerose risorse presenti su Docsity
Prepara i tuoi esami con i documenti condivisi da studenti come te su Docsity
Trova i documenti specifici per gli esami della tua università
Preparati con lezioni e prove svolte basate sui programmi universitari!
Rispondi a reali domande d’esame e scopri la tua preparazione
Riassumi i tuoi documenti, fagli domande, convertili in quiz e mappe concettuali
Studia con prove svolte, tesine e consigli utili
Togliti ogni dubbio leggendo le risposte alle domande fatte da altri studenti come te
Esplora i documenti più scaricati per gli argomenti di studio più popolari
Ottieni i punti per scaricare
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Una introduzione alla complessità computazionale asintotica degli algoritmi, illustrando le notazioni o, ω e ө. Esplora il concetto di limite asintotico superiore, inferiore e stretto, fornendo esempi e regole per il calcolo della complessità. Anche il tempo di esecuzione degli algoritmi in funzione della loro complessità temporale, evidenziando l'importanza dell'analisi asintotica per la valutazione delle prestazioni degli algoritmi.
Tipologia: Dispense
1 / 11
Questa pagina non è visibile nell’anteprima
Non perderti parti importanti!







Per poter valutare l’efficienza di un algoritmo, così da poterlo confrontare con algoritmi diversi che risolvono lo stesso problema, bisogna essere in grado di valutare l’ordine di grandezza del suo tempo di esecuzione e delle sue necessità in termini di memoria. In particolare, tale valutazione ha senso quando la dimensione dell’input è sufficientemente grande.
La disciplina che si occupa della quantificazione di tali grandezze è la complessità computazionale asintotica :
Salvo ove sia diversamente specificato, la grandezza che viene valutata è il tempo di esecuzione dell’algoritmo.
Prima di entrare nel vivo della valutazione della complessità computazionale asintotica degli algoritmi è necessario introdurre alcune definizioni.
Date due funzioni f(n), g(n) ≥ 0 si dice che
f(n) è un O(g(n))
se esistono due costanti c ed n 0 tali che
0 ≤ f(n) ≤ c g(n) per ogni n ≥ n 0
Esempio 2. Sia f(n) = 3n + 3
f(n) è un O(n^2 ) in quanto, posto c = 6, cn^2 ≥ 3n + 3 per ogni n ≥ 1.
Ma f(n) è anche un O(n) in quanto cn ≥ 3n + 3 per ogni n ≥ 1 se c ≥ 6 , oppure per ogni n ≥ 3 se c ≥ 4.
E' facile convincersi che, data una funzione f(n) , esistono infinite funzioni g(n) per cui f(n) risulta un O(g(n)). Come sarà più chiaro in seguito, ci interessa tentare di determinare la funzione g(n) che meglio approssima la funzione f(n) dall'alto o, informalmente, la più piccola funzione g(n) tale che f(n) sia O(g(n)).
Esempio 2. Sia f(n) = n^2 + 4n
Ebbene, f(n) è un O(n^2 ) in quanto cn^2 ≥ n^2 + 4n per ogni n se c ≥ 5 , oppure per ogni n ≥ 4/(c-1) se c > 1.
Esempio 2.
Dimostriamo, per induzione su m , che f(n) è un O(nm).
Casi base
Ipotesi induttiva
h(n) = è un O(nm-1) se c ≥
Passo induttivo
Dobbiamo dimostrare che
f(n) = è un O(nm)
cioè che
c’nm^ ≥ se c’ ≥
Per l’ipotesi induttiva:
Abbiamo visto come, in entrambe le notazioni esposte in precedenza, per ogni funzione f(n) sia possibile trovare più funzioni g(n). In effetti O(g(n)) e Ω(g(n)) sono insiemi di funzioni , e dire “ f(n) è un O(g(n) ” oppure “ f(n) = O(g(n)” ha il significato di “ f(n) appartiene a O(g(n)) ”.
Tuttavia, poiché i limiti asintotici ci servono per stimare con la maggior precisione possibile la complessità di un algoritmo, vorremmo trovare – fra tutte le possibili funzioni g(n) – quella che più si avvicina a f(n).
Per questo cerchiamo la più piccola funzione g(n) per determinare O e la più grande funzione g(n) per determinare Ω.
La definizione che segue formalizza questo concetto intuitivo.
Date due funzioni f(n) , g(n) ≥ 0 si dice che
f(n) è un Ө(g(n))
se esistono tre costanti c 1 , c 2 ed n 0 tali che
c 1 g(n) ≤ f(n) ≤ c 2 g(n) per ogni n ≥ n 0
In altre parole, f(n) è Ө(g(n)) se è contemporaneamente O(g(n)) e Ω(g(n)).
Esempio 2. Sia f(n) = 3n + 3
f(n) è un Ө(n) ponendo, ad esempio, c 1 = 3, c 2 = 4, n 0 = 3.
Esempio 2. Sia f(n) un polinomio di grado m: f(n) = , con am > 0
La dimostrazione che f(n) è un Ө(nm) discende dagli esempi 2.3 e 2.5.
Per semplificare il calcolo della complessità asintotica degli algoritmi si possono sfruttare delle semplici regole che dapprima enunciamo chiarendole con degli esempi, ed in un secondo momento dimostriamo.
Regole sulle costanti moltiplicative
1A : Per ogni k > 0 e per ogni f(n) ≥ 0 , se f(n) è un O(g(n)) allora anche k f(n) è un O(g(n)).
1B : Per ogni k > 0 e per ogni f(n) ≥ 0 , se f(n) è un Ω(g(n)) allora anche k f(n) è un Ω(g(n)).
1C : Per ogni k > 0 e per ogni f(n) ≥ 0 , se f(n) è un Ө(g(n)) allora anche k f(n) è un Ө(g(n)).
Informalmente, queste tre regole si possono riformulare dicendo che le costanti moltiplicative si possono ignorare.
Regole sulla commutatività con la somma
2A : Per ogni f(n), d(n) > 0 , se f(n) è un O(g(n)) e d(n) è un O(h(n)) allora f(n)+d(n) è un O(g(n)+h(n)) = O(max(g(n),h(n))).
2B : Per ogni f(n), d(n) > 0 , se f(n) è un Ω(g(n)) e d(n) è un Ω(h(n)) allora f(n)+d(n) è un Ω(g(n)+h(n)) = Ω(max(g(n),h(n))).
2C : Per ogni f(n), d(n) > 0 , se f(n) è un Ө(g(n)) e d(n) è un Ө(h(n)) allora f(n)+d(n) è un Ө(g(n)+h(n)) = Ө(max(g(n),h(n))).
Informalmente, queste tre regole si possono riformulare dicendo che le notazioni asintotiche commutano con l’operazione di somma.
Regole sulla commutatività col prodotto
3A : Per ogni f(n), d(n) > 0 , se f(n) è un O(g(n)) e d(n) è un O(h(n)) allora f(n)d(n) è un O(g(n)h(n)).
3B : Per ogni f(n), d(n) > 0 , se f(n) è un Ω(g(n)) e d(n) è un Ω(h(n)) allora f(n)d(n) è un Ω(g(n)h(n)).
Regola 2A. Per ogni f(n), d(n) > 0 , se f(n) è un O(g(n)) e d(n) è un O(h(n)) allora f(n)+d(n) è un O(g(n)+h(n)) = O(max(g(n),h(n))).
Dim. Se f(n) è un O(g(n)) e d(n) è un O(h(n)) allora esistono quattro costanti c’ e c” , n’ 0 ed n” 0 tali che:
f(n) ≤ c’g(n) per ogni n ≥ n’ 0 e d(n) ≤ c”h(n) per ogni n ≥ n” 0
allora:
f(n) + d(n) ≤ c’g(n) + c”h(n) ≤ max(c’, c”)(g(n) + h(n)) per ogni n ≥ max(n’ 0 , n” 0 )
Da ciò segue che f(n) + d(n) è un O(g(n)+h(n)).
Infine:
max(c’, c”)(g(n) + h(n)) ≤ 2 max(c’, c”) max(g(n), h(n)).
Ne segue che f(n) + d(n) è un O(max(g(n), h(n))). CVD
Regola 3A. Per ogni f(n), d(n) > 0 , se f(n) è un O(g(n)) e d(n) è un O(h(n)) allora f(n)d(n) è un O(g(n)h(n)).
Dim. Se f(n) è un O(g(n)) e d(n) è un O(h(n)) allora esistono quattro costanti c’ e c” , n’ 0 ed n” 0 tali che:
f(n) ≤ c’g(n) per ogni n ≥ n’ 0 e d(n) ≤ c”h(n) per ogni n ≥ n” 0
allora:
f(n)d(n) ≤ c’c”g(n)h(n) per ogni n ≥ max(n’ 0 , n” 0 )
Da ciò segue che f(n)d(n) è un O(g(n)h(n)). CVD
Le dimostrazioni delle altre regole che coinvolgono le notazioni Ω e Ө sono lasciate per esercizio.
Vediamo ora come calcolare effettivamente la complessità computazionale di un algoritmo, adottiando il criterio della misura di costo uniforme descritto nel par. 1.4.2.
Prima di procedere, facciamo due importanti considerazioni.
Innanzi tutto, osserviamo che è ragionevole pensare che la complessità computazionale, intesa come funzione che rappresenta il tempo di esecuzione di un algoritmo, sia una funzione monotona non decrescente della dimensione dell'input. Questa osservazione ci conduce a constatare che, prima di passare al calcolo della complessità, bisogna definire quale sia la
dimensione dell'input. Trovare questo parametro è, di solito, abbastanza semplice: in un algoritmo di ordinamento esso sarà il numero di dati, in un algoritmo che lavora su una matrice sarà il numero di righe e di colonne, in un algoritmo che opera su alberi sarà il numero di nodi, ecc.. Tuttavia, vi sono casi in cui l’individuazione del parametro non è banale; in ogni caso, è necessario stabilire quale sia la variabile (o le variabili) di riferimento prima di accingersi a calcolare la complessità.
In secondo luogo, vogliamo sottolineare che la notazione asintotica viene sfruttata pesantemente per il calcolo della complessità computazionale degli algoritmi, quindi - in base alla definizione stessa – tale complessità computazionale potrà essere ritenuta valida solo asintoticamente.
In effetti, esistono degli algoritmi che per dimensioni dell'input relativamente piccole hanno un certo comportamento, mentre per dimensioni maggiori un altro. Sottolineiamo che siamo interessati all'andamento asintotico della complessità.
Per poter valutare la complessità di un algoritmo, esso deve essere formulato in un modo che sia chiaro, sintetico e non ambiguo.
Si adotta il cosiddetto pseudocodice , che è una sorta di linguaggio di programmazione “informale” nell’ambito del quale:
Non esiste una notazione universalmente accettata per lo pseudocodice. In queste dispense (come del resto nel libo di testo) useremo l’indentazione per rappresentare i diversi livelli dei blocchi di istruzioni, useremo il simbolo per indicare un’assegnazione, il simbolo = per verificare che il contenuto di 2 variabili sia lo stesso e il simbolo ≠ per verificare che il contenuto di 2 variabili sia differente.
Le regole generali che si adottano per valutare la complessità computazionale di un algoritmo sono le seguenti:
Esempio 2. Calcolo del massimo in un vettore disordinato contenente n valori.
Funzione Trova_Max (A: vettore)
1 max A[1]
2 for i = 2 to n do
3 if A[i] > max
4 then max A[i]
5 stampa max
Ө( 1 ) (n – 1) iterazioni più Ө( 1 ) Ө( 1 ) Ө( 1 ) Ө( 1 )
La complessità dell’istruzione 1 è Ө(1).
L’iterazione viene eseguita (n – 1) volte, e ciascuna iterazione (costituita dalle istruzioni 2, 3 e 4) ha complessità Ө(1) + Ө(1) + Ө(1) = Ө(1) poiché l’incremento del contatore (istr. 2), la valutazione della condizione (istr. 3) e l’assegnazione (istr. 4) sono istruzioni elementari.
La complessità dell’istruzione 5 è Ө(1).
Quindi, detta T(n) la complessità temporale di questo algoritmo, essa vale:
T(n) = Ө(1) + (n – 1) Ө(1) + Ө(1) = Ө(n)
Concludiamo questo argomento mostrando come variano i tempi di esecuzione di un algoritmo in funzione della sua complessità temporale.
Ipotizziamo di disporre di un sistema di calcolo in grado di effettuare una operazione elementare in un nanosecondo ( 109 operazioni al secondo), e supponiamo che la dimensione del problema sia n = 106 (un milione):
C’è un'altra situazione interessante da considerare: che succede se la complessità cresce esponenzialmente, ad esempio quando è O(2n)?
E’ abbastanza ovvio che i tempi di esecuzione diventano rapidamente proibitivi: un tale tipo di problema su un input di dimensione n = 100 richiede per la sua soluzione mediante il sistema di calcolo di cui sopra ben 1,2610^21 secondi, cioè circa 310^13 anni.
Si può pensare che l’avanzamento tecnologico, per quanto formidabile, possa rendere abbordabile un tale problema? Purtroppo la risposta è no. Infatti, poniamoci la seguente domanda: supponendo di avere un calcolatore estremamente potente che riesce a risolvere un problema di dimensione n = 1000 , avente complessità O(2n) , in un determinato tempo T , quale dimensione n’ = n + x del problema riusciremmo a risolvere nello stesso tempo utilizzando un calcolatore mille volte più veloce?
Possiamo scrivere la seguente uguaglianza:
T = =
Si ha quindi:
Ossia
x = log 1000 ≈ 10
Dunque, con un calcolatore mille volte più veloce riusciremmo solo a risolvere, nello stesso tempo, un problema di dimensione 1010 anziché di dimensione 1000.
In effetti esiste un’importantissima branca della teoria della complessità che si occupa proprio di caratterizzare i cosiddetti problemi intrattabili , ossia quei problemi la cui complessità computazionale è tale per cui essi non sono né saranno mai risolubili per dimensioni realistiche dell’input.