Teoria della Calcolabilità
non vediamo
problema di base:
si può fare con un algoritmo/programma ??
possibile / impossibile  finito / infinito
possibile =
si può fare in un tempo finito
usando quantità finita di memoria .................
usando una quantità finita di risorse
esempi:
trovare tutti i numeri primi minori di N
possibile
trovare tutti i numeri primi
impossibile
calcolare le prime N cifre decimali di 
possibile
calcolare tutte le cifre decimali di 
impossibile
decidere se una formula con and, or, not è sempre vera
possibile
idem per formule che usano anche  
impossibile
obiettivo primario
precisare i concetti di:
algoritmo
funzione calcolabile con un algoritmo
problema risolubile con un algoritmo
1
Teoria della Complessità
problema di base:
sapendo che si può fare
quanto è difficile/complesso, quanto costa farlo ??
obiettivo primario
precisare i concetti di:
1) complessità/costo di un algoritmo o programma
2) difficoltà intrinseca (o costo intrinseco) del calcolo di una
funzione o della risoluzione di un problema.
Vedremo solo punto 1)
Inoltre, non vedremo la teoria della complessità, ma soltanto
complessità computazionale concreta
Qui
programma =
algoritmo =
programma in un linguaggio di
programmazione vero
descrizione in linguaggio comodo ma
preciso della soluzione del problema
2
A che serve lo studio della complessità ?
Un problema  tante soluzioni
Come scegliere ?
Almeno tre criteri:
1) semplicità, chiarezza .....
2) generalità, riusabilità ....
3) efficienza:
tempo di esecuzione
quantità di memoria necessaria
quantità di traffico generato su rete .......
Però
diverse esigenze
1) minimizzare
2) minimizzare
3) minimizzare
anche in contrasto:
tempi /costi di sviluppo del programma
costi di manutenzione del programma
costi di esecuzione
del programma
1) programma usa e getta
3) parte critica di sistema operativo, foglio elettronico, ...,
applicazioni real-time (la risposta deve arrivare entro un
certo tempo, se arriva dopo, è inutile)
2) .................
tutti gli altri casi
studio complessità algoritmi / programmi
strumento per
stimare costi di esecuzione
anche tecniche di benchmarking (tests su input significativi)
di analisi del profilo
che però NON vediamo
3
Misure di complessità per algoritmi e programmi
piú chiaro sui programmi ..........
Misure statiche o strutturali:
lunghezza del programma quante righe di codice
annidamento di cicli, procedure,.... .
si guarda la complessità della struttura del programma
interesse teorico
non le consideriamo e ci limitiamo a .........
Misure dinamiche o computazionali
misurare
quantità di risorse utilizzate
dal programma in esecuzione
Risorse
diversi tipi ( tempo di CPU, quantita` di RAM,
quantità di memoria secondaria,.................)
ci limitiamo al tempo
4
Misurare il tempo di esecuzione .........
Esempio: programma P per ordinare successione di interi
Tempo di esecuzione di P su input
i1, ...., in
dipende almeno da:
1) n
meglio 100 che 100 000
2) la successione i1, ...., in
quasi ordinata
molto "disordinata" ........
3) algoritmo astratto (il metodo di ordinamento scelto), sia A
4) linguaggio di programmazione usato
5) il sistema su cui si esegue il programma
hardware
processore, bus, ...
software
sistema operativo, compilatore,...
Il programmatore può agire su 3)
un po' su 4)
forse su 5)
Rispetto a 4) e 5), il fattore 3) è “a monte”
se A non va
Quindi:
NON si rimedia con linguaggio / macchina
analisi di complessità
solo il fattore 3), in funzione di 1) e 2)
La prima analisi è questa; permette di concludere, ad es, che un metodo di ordinamento è migliore/peggiore di un altro.
In situazioni critiche (es. applicazioni real-time, dove tempo è parte della correttezza) si passa a analisi che tiene
conto di 4) e 5); ad esempio, codificato l’algoritmo come programma, si valutano prestazioni reali tramite benchmark.
5
NOI: solo 3) in funzione di 1) e 2)
nell’esempio, invece di
(*) tempo di esecuzione di P su input i1, ...., in
considereremo
(**) tempo di esecuzione di A su input i1, ...., in
problema:
tempo in (*)
tempo in (**)
può essere tempo fisico,
no
in (**) non si misura il tempo, ma qualcos’altro
In effetti, (**) diviene:
(***) numero di passi necessari per eseguire A su i1, ...., in
Però ora bisogna capire cosa sono i passi !!
facile a livello di linguaggio macchina (JUMP, LOAD...)
ma noi ragioniamo a livello di pseudo-codice ....
o si cerca di capire grosso modo .....
o si usano ordini di grandezza:
proporzionale ad n2
invece di
= 75 n2 - 33 n + 58
Idea di base:
istruzione semplice  num costante di passi
le costanti sono tutte uguali in ordini di grandezza
6
Funzioni complessità
Es.
ordinare una successione finita di numeri
programma P algoritmo astratto A
INPUT_per_A
insieme degli input su cui A termina
Per esprimere
“tempo” (il costo) di esecuzione di A sull’input i1, ...., in
espresso come “numero di passi di calcolo”
possiamo usare funzione
TiA : INPUT_per_A  N
funzione non facile da calcolare, anche in modo approssimato
n numeri distinti ==> n! input diversi
input diversi
==> costi diversi ( es: da n a n2 )
Inoltre interessa: quanto costa ordinare n numeri (qualunque)
funzione:
TA : N  N
t.c.
TA (n) = costo di ordinare n numeri.
Come definirla ?
caso peggiore:
TA (n) = max { TiA (i) | i successione di n numeri }
caso migliore:
TA (n) =
min
{ ....... }
caso medio:
TA (n) =
media { ....... }
7
Come ottenere TA se non si conosce
Per caso peggiore
TiA ?
fissato n:
si comincia a ragionare su generico input di dimensione n
appena diventa necessario, si cerca input i di lunghezza n,
che sia pessimo .......
Per caso migliore .................. input ottimo
Tutto questo è facilitato dal fatto che non si cerca di calcolare
esattamente TA , ma solo di valutarne l’ordine di grandezza.
Nel seguito considereremo solo il caso peggiore
Qui esempio di InsertionSort : vedere file a parte
8
Ordini di grandezza: O, e 
Consideriamo funzioni da N in R (l’insieme dei reali)
definite e non-negative da un certo punto in poi
Nel seguito:
f, g, .... sono funzioni di questo tipo
es
f (n) = 5 log 2 (n) - 10
f(0) indefinito
f(1) negativo
......................
f(n) definito e non negativo
=============
Def. di O(g)
per n ≥ 4
[si legge " O di g"]
O(g) = { f | esistono
c  R, c > 0,
per ogni n ≥ n0 :
t.c.
ed n0  N,
f(n) ≤ c g(n)
}
=============
es
sia g t.c. g(n) = 3 n2 + 10 n - 5
O(g) contiene
f
t.c.
f(n) = 300 n2
h
t.c.
h(n) = 100000000 n + 500000
k
t.c.
k(n) = 5 n log 2 (n) + 10 n
=============
f  O(g)
generalizza f ≤ g
f  O(g)
si può leggere cosí:
[ f(n) ≤ g(n), per ogni n ]
f ≤ g da un certo punto in poi ed a meno di una
costante moltiplicativa.
9
Notare che in
O(g) = { f | esistono
t.c.
c  R, c > 0,
per ogni n ≥ n0 :
ed n0  N,
f(n) ≤ c g(n)
}
c ed n0 dipendono da f
possiamo sempre prendere c razionale, di solito c > 1
si poteva anche scrivere: c f(n) ≤ g(n) ( con c < 1 )
Def. di (g) e di (g)
[si leggono : "omega di g" e "teta di g" ]
(g) = def { f |
esistono c  R, c > 0, ed n0  N,
t.c. per ogni n ≥ n0 :
g(n) ≤ c f(n) }

(g) = def O(g)  (g).
=======================
se f  O(g) si dice che
oppure che
f cresce al piú come g
g è un limite superiore per f
se f  (g) si dice che
oppure che
f cresce almeno come g
g è un limite inferiore per f
f  (g)
f cresce come g
f stesso ordine di grandezza di g
si dice che
oppure che
10
Aspetto intuitivo
f  O(g)
generalizza f ≤ g
e si può leggere :
f ≤ g da un certo punto in poi ed a meno di
una costante moltiplicativa
o, meglio:
da un certo punto in poi ed a meno di una costante
moltiplicativa f non cresce piú di g
f  (g)
generalizza f ≥ g
e si può leggere :
f ≥ g da un certo punto in poi ed a meno di
una costante moltiplicativa
o, meglio:
da un certo punto in poi ed a meno di una costante
moltiplicativa g non cresce piú di f
f  (g)
generalizza f = g
e si può leggere :
f = g da un certo punto in poi ed a meno di
una costante moltiplicativa
o, meglio:
da un certo punto in poi ed a meno di costanti
moltiplicative f cresce come g
11
Proprietà di base
f  O(f)
e analogamente per  e 
f  X(g) e g  X(h) ===> f  X(h)
           transitività
f  O(g)
sse
g  (f)
f  (g)
sse
g  (f)
sse
riflessività
X = O, , 
(g) = (f)
(g) = { f |  c, d > 0 ed n0 t.c.  n ≥ n0 :
c g(n) ≤ f(n) ≤ d g(n)
}
C'è un collegamento con il concetto di limite
(che non vediamo) .....
12
Notazione semplificata
per O, 
Invece di
f  (g)
si scrive
f(n)  (exp)
Es.
invece di
si scrive
dove g è tale che
g(n) = exp
[ anche f(n) = (exp) ]
TA  (g), dove g è t.c. g(n) = 2n
TA (n)  (2n)
Logaritmi:
si scrive
TA(n)  (log n)
senza specificare la base
infatti:
qui le costanti “non contano”
logax = c logbx
dove c = loga b
(supponiamo a, b, x t.c i log siano definiti)
13
Esempi (usando la notazione semplificata).
Supponiamo che i coefficienti, le basi dei logaritmi,.... siano tali
da rispettare le proprietà di definitezza e non-negatività
Inoltre:
k, a, b, .... sono costanti
k è intero
pk(n) è un polinomio di grado k a coefficienti reali
pk(n) = ak nk + ak-1 nk-1 + ... + a1 n + a0
1.
(1) = (a)
2.
pk(n)  O( nk )
per ogni
pk(n)  (nk)
quindi:
a strettamente positiva
se ak > 0
pk(n)  (nk)
3.
(log n)k  O(n)
4.
log nk  (log n)
5.
log(n5 an)
se ak > 0
per ogni k ≥ 0
infatti log nk = k log n
= log n5 + log an
= 5 log n + n log a
 (n)
14
sempre con :
pk(n) = ak nk + ak-1 nk-1 + ... + a1 n + a0
6.
pk(n) O(an)
7.
an O(bn)
per ogni k ed ogni a > 1
e
an (bn)
se 1 < a < b
8.
f(n) = n2 quando n è pari
n3 quando n è dispari
allora
f(n)  O(n3) (n2)
e non si può precisare meglio
se voglio usare solo monomi ( tipo nk )
15
O( ), ( ) e ( ) e funzioni complessità.
A
algoritmo di ricerca binaria in un array ordinato;
funzione complessità :
TA : N  N
facendo i conti si vede che
TA(n) = a * parte_intera(log 2 n) + b
Calcolare esattamente
a
e
b non è facile .....
ci basta capire che a > 0 e quindi il logaritmo compare davvero.
Se non conosciamo a
e
b inutile tirarsele dietro
è anche seccante scrivere “parte_intera”
Quindi, semplicemente:
TA (g) , dove g è tale che
oppure:
g(n) = log2 (n)
TA (n) ( log n )
Risultato semplice
però info sufficientemente precisa su
come cresce TA(n) al crescere di n.
permette di distinguere, ad esempio, ricerca binaria da algoritmo
ovvio S di ricerca sequenziale:
TS(n) = c n + d ,
cioè:
c d costanti, c >0
TS (n) ( n )
16
Semplicità e precisione con O  
T (n) = 18 n3 log3 n+ 5 n2
Esempio
Semplicita` :
uso O( ), ( ) e ( ) per semplificare
quindi:
T(n) (27 n3 log7 n + 44 n2 -21 n + 33 log5 n )
NO
anche se giusto
T(n) ( n3 log n )
Precisione:
SI'
uso O( ), ( ) e ( ) per avere
info significative su come cresce la
complessità al crescere della dimensione
quindi
T(n) (log n )
NO
T(n) (n27 )
NO
anche se giusto
Però
anche
n1.88 (n2)
n1.88 (n)
SI'
SI'
Sempre per precisione cerchiamo stime in ( )
ripiegando su O( ) ed ( ) quando non ci riusciamo
17
Ancora sulla def
di O( ) ( ) ( )
Vediamo perchè consideriamo
funzioni da N in R (l’insieme dei reali)
definite e non-negative da un certo punto in poi
e vediamo il ruolo di c, n0 nella def di O(g)
O(g) = { f | esistono c  R, c > 0, ed n0  N,
t.c.
per ogni n ≥ n0 :
f(n) ≤ c g(n)
}
ad esempio consideriamo TA(n) = a * parte_intera(log 2 n) + b
codominio R e non N
per eliminare “parte intera”
e simili
ruolo di n0
trascurare casi in cui
funzione indefinita
trascurare casi iniziali (input di piccole dimensioni)
costante c
permette di semplificare
trascurando
costanti moltiplicative
termini di grado inferiore
Analogo per ( ) e ( )
18
Proprietà della somma e “del massimo”
Sia
f+g
la funzione tale che f+g(n) = f(n)+g(n)
max{f,g} la funzione tale che
max{f,g}(n) = max{f(n), g(n)}
allora
fj  O(gj) ===> f1+f2  O(g1 + g2)
fj  (gj) ===> f1+f2  (g1 + g2)
f+g  ( max{f,g} )
Quindi:
 ( n log n )
3 n log n + 1000 n
Classi notevoli di funzioni
(1)
funzioni costanti
notare che (1) ≠ (0)
O(n)
funzioni al piu' lineari
O(n)  O( n2 )  O( n3 )  ....
funzioni (al piú) polinomiali
le altre:
sovrapolinomiali
esponenziali
o
sbrigativamente
19
Significato delle stime con ordini di grandezza
1 problema
3 algoritmi per risolverlo:
A1
T1(n)  (n)
A2
T2(n)  (n)
A3
T3(n)  (n).
(complessità-tempo, caso peggiore)
•
Per n piccolo
ad es.
poca differenza
per
n=3
n > n
quindi se ho solo input "piccoli" servono analisi piú fini
( contare davvero il numero di passi; fare testing; .... )
•
Per n sempre piú grande
( comportamento asintotico delle funzioni T )
differenze sempre maggiori
costanti e dei termini di ordine inferiore non contano
Il primo algoritmo è migliore degli altri (nel caso peggiore).
Per capire meglio ...... (pagina seguente)
20
Supponiamo
T1(n) = n
T2(n) = n
tempo per "un passo"
T3(n) = n
1s
=
10-6 sec
Allora
T(n)
input di dim = 10
input di dim = 60
A1
n
= 10-5 secondi
= 6 * 10-5 secondi
A2
n
= 0.1 secondi
≈ 13 minuti
A3
n
≈ 10-3 secondi
≈ 366 secoli
Per vedere che algoritmo astratto è fattore principale:
con
nuova macchina 1000 volte piú veloce
da
Inoltre
366 secoli
ci sono algoritmi in
a
36 anni ...........
22n
o peggio .....
===================
Ordini di grandezza e buon senso
Gli ordini di grandezza non devono farci trascurare le cose ovvie:
quello che è inutile resta inutile
quello che è stupido resta stupido,
anche se “in ordini di grandezza non fa differenza”.
21
Dimensione dell’input
Problema:
trovare uno o piú parametri che caratterizzano
la dimensione degli input di A.
Per un vero programma:
Ma vogliamo rimanere
quindi:
input <----> stringa
dimensione <----> lunghezza
a livello di algoritmo " astratto "
vicini al problema da risolvere
numero di elementi da ordinare
invece di
lunghezza della stringa che li codifica
(dipende dalla base, dai separatori,....)
Conseguenza:
non abbiamo una definizione di dimensione dell’input
per ogni problema dobbiamo capire ..........
Per fortuna, di solito è abbastanza facile .......
negli esercizi verra` detto
Nota: i parametri sono sempre in N.
22
Altro aspetto : individuazione dell’input !
Algoritmo A sotto forma di procedura o funzione:
l’input corrisponde ai parametri attuali
o ad alcuni di essi
ma anche a delle variabili globali.......
Algoritmo di merge sort
===> procedura MS con tre parametri:
A di tipo array [1..n] of integer
inf e sup di tipo integer
per ordinare AAA si chiama MS(AAA, 1, n)
l’input corrisponde ad AAA
Algoritmo di insertion sort ===> procedura
con un parametro A di tipo array ==> input come sopra
senza parametri, riferendosi ad un array A globale
l’input corrisponde ad A
Nei casi dubbi .......
ignorare come viene presentato A
e riferirsi direttamente al problema.
23
Regole empiriche per il calcolo di complessità
Idea di base
exp ed istruzioni “elementari” : abbiamo idea del costo
exp e istruzioni non elementari : si possono tradurre in una
successione di istruzioni elementari.
Scriviamo:
1)
tempo(....) per abbreviare
“tempo necessario a valutare/ eseguire .......”
Espressioni
tempo(exp) = costante
eccetto:
exp con chiamate a funzioni
exp con operazioni su blocchi di dati
(es. aa+bb con aa, bb arrays)
tempo(exp con chiamate a funzioni) =
 tempi(chiamate delle funzioni)
tempo(exp con operazioni su blocchi di dati) =
 tempi(operazioni “semplici”)
es: tempo(aa + bb) =  (i = inf,..., sup) tempo(aa[i] + bb[i])
se aa, bb : array [inf .. sup] of .......
24
2)
Assegnazione, return, espressioni-istruzioni stile C
tempo(x  exp) = tempo(exp)
tempo(aa[ exp’ ]  exp) = tempo(exp) + tempo(exp’)
tempo(rr.campo)  exp) = tempo(exp)
rr è un record
ma: tempo(aa bb) =  (i = inf,..., sup) tempo(aa[i]  bb[i])
se aa, bb : array [inf .. sup] of .......
tempo( return(exp) ) = tempo (exp)
tempo( exp; ) = tempo (exp)
questo per lo stile C
3) Input/output
Qui le cose si complicano. A voler essere precisi:
tempo( scrivi(exp) )
tempo ( leggi (x) )
??? perche` I/O
= tempo(exp) + ???
= ???
genera chiamate al sistema operativo ....
Per semplificarci la vita, nei conti useremo
??? = costante
25
4) Successione di istruzioni
tempo( istr_1; .... ; istr_k) = tempo(istr_1) + .... + tempo(istr_k)
poichè interessano gli ordini di grandezza si può semplificare
prendendo
max { tempo(istr_i) }
5)
Istruzioni condizionali
Se
T = tempo( if cond then blocco_1 else blocco_2)
Tc = tempo(cond)
T_i = tempo(blocco_i) per i=1, 2
allora:
Tc + min { T_1, T_2 } ≤ T ≤ Tc + max{ T_1, T_2 }
Con la filosofia del caso peggiore: T = Tc + max{ T_1, T_2 }.
Quindi: tempo( if cond then blocco_1) = Tc + T_1
26
6) Istruzione while
tempo( while cond do blocco ) =
Tc_last +  (i = 1...max) ( Tc_i + Tb_i )
dove: max = numero di volte che si ripete il ciclo
Tb_i = tempo del blocco alla i-ma iterazione
Tc_i = tempo della condizione alla i-ma iterazione
Tc_last = tempo della condizione l’ultima volta
Problema
stimare
capire
max
Tb_i
Tc_i
al variare di i
(*)
spesso non si riesce a farlo con precisione
si approssima usando ordini di grandezza.
(*) i conta le iterazione, non necessariamente compare nel while
Repeat: del tutto analogo a while.
27
Istruzioni “per”, “for”.
Il modo più sicuro
fino a quando non si è acquistata abbastanza pratica
è di tradurle usando un while, o un diagramma
Pseudocodice - Pascal
per
for
k = e_inf, e_inf+1, ...., e_sup : blocco
oppure
k := e_inf to e_sup do blocco
è equivalente a:
sup  e_sup
k  e_inf
while k ≤ sup do { blocco ;
i
k++ }
il costo
        (k = inf ... sup) ( t_blocco_k)
Linguaggio C
for( exp_1; exp_2; exp_3) <blocco>
è equivalente a:
exp_1 ;
while exp_2 do
{
<blocco>
exp_3 ;
}
28
Regole empiriche per Procedure non ricorsive
Il costo da valutare è quello della chiamata
dichiaraz:
procedura P (parf_1, ..., parf_k)
chiamata
P (para_1, ...., para_k);
tempo ( P (para_1, ....) )
=
{ blocco }
tempo (passaggio dei parametri)
+ tempo( blocco )
tempo( blocco )
si calcola applicando le regole che stiamo descrivendo;
se contiene delle chiamate a procedura/funzione, il loro costo si calcola a parte,
separatamente,.... e poi si somma tutto.
Poichè non c’è ricorsione, prima o poi si arriva ad eliminare tutte le chiamate
tempo (passaggio dei parametri)
parf_i parametro OUT o IN-OUT
calcolare indirizzo di para_i e “passarlo alla proc”.
tempo costante, ma attenzione a
para_i = aa[ exp, exp’ ], con aa array
comunque simile ad assegnazione.
parf_i parametro IN
come assegnazione parf_i  para_i
Funzioni non ricorsive
analogo (all'interno di una espressione)
Procedure / funzioni ricorsive : non vediamo
29
Altre istruzioni
in analogia a quanto visto; ad esempio:
switch/case:
si traduce in una cascata di if then else,
oppure si ragiona direttamente ......
si valuta questo, poi si confronta, poi ......
break:
e` un JUMP, quindi costo costante
new / malloc / calloc / free / dispose:
come per input/output: per semplicita` a costo costante
istruzioni ad alto livello
tipo:
“ordina l’array A in modo crescente”
“test: le due stringhe sono uguali ?”
capire come si traducono usando istruzioni standard
fare i conti sulla traduzione.
Dichiarazioni
di solito si ignorano, per due motivi:
costo costante, anche nel caso di array molto grande,
a meno che l’implementazione non preveda una
inizializzazione automatica
in quasi tutti gli algoritmi sensati, il costo delle istruzioni è
dominante.
30
Preprocessing, Compilazione, Linking,....
quindi anche
#include
#define
i costi legati a queste fasi vengono ignorati
perche`
dipendono dal sistema e dall’ambiente di programmazione
precedono l’esecuzione
Ricordiamo che:
nella realtà, si fanno i conti solo su algoritmi che,
tradotti in programmi, verranno usati molto
(si compila "una volta sola" .......)
il conto di complessità “tempo” ha per scopo
stimare ordine di grandezza del
tempo di esecuzione del codice oggetto
NON del tempo di compilazione
e nemmeno del costo di sviluppo
Esercizi:
fare i conti di complessità per gli algoritmi sulle dispense
ignorando i limiti sul numero di elementi tipo n < MAX
in altre parole, mettere MAX = valore arbitrario ....
31
Conti “ad occhio” Approssimazioni successive
Per evitare errori che nascono da conti
oppure
troppo superficiali
troppo dettagliati
fare un conto ad occhio, semplificando ....
fare un conto più preciso, ma senza perdersi troppo in
dettagli
confrontare il risultato col precedente; se non quadra capire
dove si è sbagliato (in 1 o in 2 ?) e correggere
fare un conto ancora più preciso
confrontare il risultato col precedente; se non quadra capire
dove si è sbagliato e correggere
eccetera ........
Anche:
iniziare con stime grossolane che permettono solo di
concludere in
O(...) oppure in
(...)
raffinare poco alla volta fino a concludere, se possibile,
con stima in (...).
32