Capitolo 3
Algoritmi e complessità.
3.1
Definizioni fondamentali
La teoria della complessità è uno strumento per determinare la qualità di procedure di calcolo definite
per la soluzione di generiche istanze di problemi. Tale teoria è basata sulla definizione di modelli formali
di calcolo (ad es. le macchine di Turing): una dettagliata e completa trattazione va certamente oltre
lo scopo di queste note. Tuttavia, alcuni concetti della teoria della complessità sono indispensabili per
valutare l’efficienza degli algoritmi; in modo particolare il tempo di esecuzione. Questa esigenza può
essere parzialmente soddisfatta introducendo in modo più informale i principali concetti della teoria della
complessità, il concetto di problema e di algoritmo. Iniziamo con l’introdurre il concetto di problema.
Definizione 3.1.1 Si definisce Problema una funzione F dall’insieme I delle istanze (INPUT) all’insieme
S delle soluzioni (OUTPUT).
Ciascuna istanza I è l’insieme delle informazioni necessarie affinché la funzione F possa determinare
in modo univoco una soluzione appartenente all’insieme S.
Data un’istanza i ∈ I, quindi, il problema associa all’istanza i la soluzione s = F (i) ∈ S. L’istanza può
quindi essere definita come l’insieme dei dati d’ingresso, mentre la soluzione è una particolare ”proprietà”
F (i) dell’istanza i.
Per illustrare il concetto di problema consideriamo un classico esempio su grafi: il problema del
cammino orientato di lunghezza minima fra due nodi dati. Tale problema viene così definito:
Definizione 3.1.2 Dato un grafo orientato G(N, A), con delle lunghezze associate agli archi, e due nodi
s e t di G, trovare il cammino orientato da s a t di lunghezza complessiva minima.
Un’istanza del problema è data in Figura 3.1, ed è costituita dal grafo G (informazioni topologiche)
e dalle informazioni metriche costituite dalle lunghezze associate agli archi del grafo. Il problema consiste nell’associare a ciascuna istanza (uno specifico grafo con delle specifiche lunghezze) il cammino di
lunghezza totale minima (soluzione). Nella stessa figura è mostrato il cammino minimo da s a t per
l’istanza data. Tale cammino è (una) soluzione del problema del cammino minimo per l’istanza data.
Si osservi che in un grafo dato potrebbero esistere più cammini di lunghezza minima. In tal caso,
la funzione assocerebbe all’istanza d’ingresso uno solo tra questi cammini. Una definizione più generale
definisce il problema come una relazione piuttosto che una funzione, e cioè l’insieme di tutte le possibili
coppie (i, s) ∈ I × S. Qui si è preferita una definizione semplificata del concetto di problema allo scopo
di alleggerire la notazione. Possiamo ora introdurre il concetto di algoritmo.
Definizione 3.1.3 Si definisce algoritmo una procedura in grado di trovare, in passi successivi, una
soluzione per una generica istanza di un problema.
18
2
v2
3
s
t
1
1
t
s
1
1
3
v1
v1
Figura 3.1: Istanza e soluzione del problema di cammino minimo
Si osservi che istanza e soluzione debbono essere opportunamente codificate, ad esempio univocamente associate a stringhe binarie (bit). Come vedremo in seguito, alcuni parametri degli algoritmi di
soluzione dipendono dalla codifica scelta per l’input e per l’output.
Un algoritmo deve godere delle seguenti proprietà:
• Correttezza. Un algoritmo deve essere corretto: applicato a un’istanza i ∈ I, l’algoritmo produce
sempre la soluzione associata F (i).
• Efficienza: l’efficienza di un algoritmo viene misurata in base al tempo necessario per risolvere il
problema e allo spazio di memoria occupato durante il calcolo.
Tipicamente, il tempo di calcolo dipenderà dalla dimensione dell’istanza di input: più grande sarà,
più tempo sarà richiesto dall’algoritmo. La dimensione dell’istanza è dunque un altro parametro rilevante
e di norma viene misurato in numero di bit necessari alla codifica. Per rendere indipendente il tempo di
calcolo dalla potenza di calcolo dello strumento sul quale viene eseguito l’algoritmo, esso viene calcolato
in termini di passi, ovvero operazioni elementari.
3.2
Misure di efficienza di algoritmi.
Per misurare l’efficienza di un algoritmo si fa uso di un modello di calcolo, ovvero di computer ideale in
grado di effetturare alcune operazioni elementari su dati contenuti in una memoria organizzata in celle
(o parole) che contengono un numero costante di bit (es. 32 bit).
cella
Figura 3.2: Cella di memoria
Ogni operazione primitiva richiede 1 passo. Le operazioni considerate primitive sono, ad esempio:
• Operazioni logico-aritimetiche (+,-, *, /, if, . . .)
19
• Acccesso a matrici e vettori (A[23], B[12, 23])
• Assegnazione di valori a variabili (A[i] := 34)
• Chiamate di funzioni e sub-routine
• Confronti tra numeri (A[i] > A[j])
• ...
I loop (es. for, while, repeat) richiedono un numero di passi che è funzione della configurazione dei
dati trattati.
Vediamo con un esempio come si esegua il conteggio dei passi di un algoritmo. Consideriamo il
problema del calcolo della componente di valore massimo di un vettore a componenti intere:
Definizione 3.2.1 Dato un vettore a d componenti intere, trovare la (una) componente di valore massimo.
L’insieme delle istanze di input sono tutti i vettori a d componenti intere. L’insieme delle soluzioni
coincide con l’insieme degli interi.
ALGORITMO
————————————–
1. M ax := A[1]
2. For i := 2 to d
do begin
3. if A[i] > M ax
4. then M ax := A[i]
end;
————————————–
Figura 3.3:
Si può adesso contare il numero di volte che ogni istruzione viene eseguita.
1. M ax := A[1]. Questa istruzione viene eseguita una sola volta. In termini di operazioni primitive, essa consiste in un’operazione di assegnazione e un’operazione di accesso a vettore. Quindi,
complessivamente, 2 operazioni primitive (2 passi).
2. For i := 2 to d. Questa istruzione viene eseguita d−1 volte. Consiste in un’operazione di incremento
e un’operazione di confronto. Quindi, complessivamente, 2 operazioni primitive (2 passi) eseguite
d − 1 volte.
3. if A[i] > M ax. Questa istruzione viene eseguita d − 1 volte. Consiste in un’operazoione di accesso
a vettore e un’operazione di confronto. Quindi, complessivamente, 2 operazioni primitive (2 passi)
eseguite d − 1 volte.
4. then M ax := A[i]. Questa istruzione viene eseguita un numero variabile di volte (in dipendenza
dal risultato del test), numero compreso fra 0 (se il test non è mai verificato, vettore ordinato per
valori decrescenti) e d − 1 (test sempre verificato, vettore ordinato per valori crescenti). Consiste
in un’operazione di assegnazione e un’operazione di accesso a vettore. Quindi, complessivamente,
2 operazioni primitive (2 passi).
20
Max := A[1]
For i:=2 to d
do begin
if A[i]> Max
then Max := A[i]
end;
[2 passi – accesso vettore+assegnazione]
[2 passi – confronto (i con d)+incremento di i]
[2 passi – accesso vettore+confronto]
[2 passi – accesso vettore+assegnazione]
d-1 volte !
Figura 3.4: Numero passi dell’algoritmo algoritmo per la componente più grande
Quanto esposto viene riassunto in Figura 3.4.
Quindi, per d > 0, il numero totale di passi (tempo) T sarà 2 + 4(d − 1) ≤ T ≤ 2 + 6(d − 1). Questo
risultato evidenzia come il numero di passi dell’algoritmo dipenda dalla dimensione d dell’istanza.
Si definisce tempo di esecuzione di un algoritmo il numero di passi necessario a determinare la soluzione
associata a un’istanza, e dipende da:
• dimensione size(i) dell’istanza i ∈ I e
• specifica istanza: nel caso del vettore visto sopra dipende dall’ordinamento delle componenti del
vettore.
Si definisce complessità di un algoritmo il numero di passi necessario all’algoritmo per determinare la
soluzione associata a una generica istanza i ∈ I avente dimensione size(i).
La complessità è quindi una funzione c(size(i)) della sola dimensione dell’istanza. Si tratta ora di
definire misure formali della dimensione di un’istanza size(i) è della complessità di un algoritmo c(size(i)).
La dimensione size(i) di un’istanza i ∈ I è definita come il numero di celle necessarie a rappresentare
i (i dati d’ingresso). Tale numero dipende a sua volta da alcuni parametri tipici dell’istanza. Ad esempio,
nel caso del vettore di interi a d componenti, supponendo che il numero di celle richieste per rappresentare
ciascun intero sia al più k, la dimensione della rappresentazione sarà kd: ovvero la dimensione dell’istanza
è, in questo caso, una funzione lineare del numero di componenti del vettore di input. Da ora in poi faremo
l’ipotesi che la cardinalità di celle necessarie per rappresentare un numero (intero, reale, ecc.) sia un valore
costante predeterminato. Questo implica che la dimensione del massimo numero rappresentabile è fissata.
Si consideri l’esempio di un generico problema di programmazione lineare: min cT x : {x : Ax = b, x ≥
0}, con A ∈ Rm×n , b ∈ Rm , c ∈ Rn : in questo caso, il numero di celle per rappresentare l’istanza size(i),
e cioè per rappresentare A, b e c, è proporzionale a mn + m + n.
Ora, per definire la complessità di un algoritmo c(size(i)) per una generica istanza di dimensione
size(i) dovremmo considerare il valor medio del tempo di esecuzione su tutte le istanze di dimensione
size(i). Questo calcolo può essere effettivamente svolto in pochi casi, ed è genericamente molto complicato. Si preferisce quindi valutare una misura di complessità diversa, la complessità nel caso peggiore, che
indicheremo con W (size(i)) e che è definita come il numero di passi necessari a determinare la soluzione
della più difficile istanza di dimensione size(i). Analogamente, possiamo definire una complessità nel
caso migliore, come il numero di passi B(size(i)) necessari ad un algoritmo per determinare la soluzione
della più facile istanza di dimensione size(i).
Gli andamenti qualitativi delle tre misure di complessità sono illustrati in Figura 3.5.
Riconsideriamo l’esempio dell’algoritmo per il calcolo della componente di valore massimo di un
vettore di interi. Si è visto che il numero di passi T dipendeva sia dalla dimensione d del vettore, che dal
risultato del test all’istruzione 3. Specificamente, si è visto che 4d − 2 ≤ T ≤ 6d − 4: quindi, nel caso
migliore sarà B(d) = 4d − 2, mentre nel caso peggiore sarà W (d) = 6d − 2.
21
W(size(I))
Valor medio
#passi
B(size(I))
size(I)
Figura 3.5: Tre misure di complessità
W(d)
B(d)
#passi
1
d
Figura 3.6: Complessità nel caso migliore e nel caso peggiore per l’algoritmo della massima
componente di un vettore
La misura di complessità maggiormente utilizzata è quella nel caso peggiore, per una serie di motivi.
In primo luogo, il caso migliore si ottiene per istanze poco significative. Al contrario, il caso peggiore si
ottiene per istanze patologiche, e rappresenta quindi una misura ”prudenziale” della complessità. Inoltre,
è in genere sufficientemente agevole determinare un’approssimazione superiore (in inglese: upper bound)
g(size(i)) della funzione W (size(i)).
g(size(I))
#passi
W(size(I))
size(I)
Figura 3.7: Upper bound g() del caso peggiore W ()
Date due funzioni f, g : Rn → R, diremo che f è ordine di g (indicato con O(g)) se e solo se esiste
una costante c1 ≥ 0 e un punto x0 ∈ Rn tali che f (x) ≤ c1 g(x) per ogni x ≥ x0 , x ∈ Rn .
In altri termini f (x) è ordine di g(x) se, per valori di x abbastanza grandi, il valore di f (x) è
definitivamente inferiore a quello di g(x).
Ad esempio, se f (x) = x2 + 100x + 10000 allora f (x) è O(x2 ). Infatti, è facile vedere che, se x ≥ 1000,
f (x) < 2x2 (si osservi che in questo caso c1 = 2). Di converso, si osservi che se, per esempio, x ≤ 10,
22
allora f (x) > 2x2 .
Una semplice regola per calcolare g(x) data f (x):
• elimina coefficienti e costanti
• conserva l’addendo di f (x) che cresce più rapidamente
Esempi di applicazione:
f (x) = 20x2 + log(x) + 10000 → O(x2 )
f (x) = 2x + 20x2 + log(x) + 10000 → O(2x )
Il prossimo teorema illustra alcune semplici relazioni conseguenza della definizione di ”O grande di”.
Teorema 3.2.2 Siano d(n), e(n), f (n) e g(n) funzioni da N+ → IR. Allora valgono le seguenti relazioni:
1. Se d(n) è O(f (n)) allora ad(n) è O(f (n)) per ogni a > 0.
2. Se d(n) è O(f (n)) e e(n) è O(g(n)) allora d(n) + e(n) è O(f (n) + g(n)).
3. Se d(n) è O(f (n)) e f (n) è O(g(n)) allora d(n) è O(g(n)).
4. Se d(n) è O(f (n)k ) e f (n) è O(g(n)) allora d(n) è O(g(n)k ).
3.3
Complessità polinomiale
Diremo che un algoritmo ha Complessità nel Caso Peggiore O(g(size(i))) se e solo se la funzione W (size(i))
è ordine di g(size(i)), ovvero: g(size(i)) è un upper bound di W (size(i)). Ad esempio, l’algoritmo per il
calcolo della componente di valore massimo di un vettore di dimensione d ha complessità nel caso peggiore
pari a W (size(i)) = W (d) = 6d − 4, e quindi la complessità è O(d).
Un algoritmo si dice avere complessità polinomiale se e solo se la sua Complessità nel Caso Peggiore
è O(size(i)k ) con k costante.
Consideriamo di nuovo la programmazione lineare. Si è visto che size(i) = mn + n + m. Se esistesse
un algoritmo A che risolvesse la programmazione lineare (nel caso peggiore) in W (size(i)) = m4 + n4 +
4mn + m3 allora, essendo W (size(i)) ≤ (mn + n + m)4 , la complessità di A sarebbe O(size(i)4 ), e quindi
A avrebbe complessità polinomiale.
Secondo la definizione di Jack Edmonds (uno dei padri della teoria) un algoritmo è efficiente (good)
se e solo se ha complessità polinomiale.
Naturalmente, il valore k in O(size(i)k ) è un indice di efficienza: un algoritmo di complessità
O(size(I)2 ) è in genere migliore di un algoritmo di complessità O(size(i)3 ).
Un algoritmo ha Complessità Esponenziale se la complessità nel caso peggiore cresce più velocemente
di size(i)k per ogni possibile k (algoritmo inefficiente). In Figura 3.8 sono tabellati i valori di alcune
funzioni di n per valori crescenti di n. Si osservi l’acceerazione della crescita della funzione quando gli
esponenti crescono e, ancora più evidentemente, per la funzione esponenziale 2n .
3.4
Codifiche e trascodifiche
Come introdotto nel Paragrafo 3.1, ogni istanza di input per uno specifico algoritmo deve essere opportunamente codificata. Ad esempio, un vettore d’interi può essere rappresentato in memoria con una
struttura dati di tipo vettore oppure con una lista semplice (vedi Figura 3.9). Nella lista semplice vengono
rappresentati unicamente gli elementi diversi da 0. Naturalmente, oltre al valore, bisogna memorizzare
per ogni elemento non nullo anche la posizione dell’elemento stesso nel vettore. Quindi, per ogni elemento
23
lo g (n)
n
1
2
3
4
5
6
7
8
9
10
2
4
8
16
32
64
12 8
2 56
512
10 2 4
n^2
n^5
4
16
64
2 56
10 2 4
4096
16 3 8 4
6 553 6
2 6 2 14 4
10 4 8 576
2 ^n
32
10 2 4
3 2 76 8
10 4 8 576
3 3 554 4 3 2
10 73 74 18 2 4
3 4 3 59 73 8 3 6 8
1.0 9 9 51E+12
3 .518 4 4 E+13
1.12 59 E+15
4
16
2 56
6 553 6
4 2 9 4 9 6 72 9 6
1.8 4 4 6 7E+19
3 .4 0 2 8 2 E+3 8
1.1579 2 E+77
1.3 4 0 8 E+154
***
Figura 3.8: Andamento di alcune funzioni di n
non nullo, la rappresentazione a lista richiede un’occupazione di 3 celle di memoria, una per rappresentare
il valore, un’altra per rappresentare l’indice e la terza per mantenere il puntatore al prossimo elemento
nella lista. Quindi, se il vettore contiene nz elementi diversi da 0, avremo bisogno di 3nz celle di memoria. Se il vettore è di dimensione m, la rappresentazione ”diretta” tramite la struttura vettore richiede
invece m celle di memoria. Dal punto di vista dell’occupazione di spazio, la rappresentazione a lista sarà
conveniente quando 3nz < m, ovvero quando il vettore è un vettore sparso.
1
2
3
4
5
6
7
8
13
0
0
12
15
0
15
0
1
13
4
12
5
15
7
15
Figura 3.9: Codifiche diverse di un vettore di interi
Un algoritmo è corretto se, data un’istanza codificata nel modo previsto dall’algoritmo stesso, fornisce
in output la soluzione univocamente associata all’istanza. Quindi, codifiche diverse della stessa istanza
corrispondono a diversi algoritmi. Ad esempio, se le istanze del problema sono vettori di interi, non
possiamo usare lo stesso algoritmo per input forniti con la rappresentazione vettoriale ed input forniti
con la rappresentazione a lista. Naturalmente, i due algoritmi possono utilizzare lo stesso metodo di
soluzione, ma si tratta comunque di algoritmi distinti.
3.4.1
Trascodifiche
Supponiamo ora di disporre di un algoritmo A che accetta istanze i ∈ I di un problema P secondo una
codifica IA di dimensione size(IA ). Che succede se la mia istanza è rappresentata con un’altra codifica,
diciamo IB ? Posso ancora utilizzare A per trovare soluzioni al problema P ? Per quanto detto sopra ciò
non è possibile. Tuttavia, se disponessimo di un ”traduttore” che trasforma IB in IA , allora potremmo
effettuare la traduzione e fornire il risultato di tale traduzione in input a A. Un traduttore è dunque
un algoritmo C che risolve il problema di trascodifica, ovvero accetta in input un’istanza codificata come
IB e fornisce in output un’istanza codificata come IA . Possiamo quindi definire un nuovo algoritmo
per il problema P che accetta istanze codificate come IB ”concatenando” l’algoritmo C e l’algoritmo A
(indicheremo tale concatenazione come C • A. L’algoritmo complessivo accetta un’istanza codificata come
24
IB , la trasforma nella stessa istanza codificata come IA , applica quindi A all’input IA e produce una
soluzione all’istanza.
IB
C •A
C
IA
A
Figura 3.10: Un algoritmo per istanze rappresentate come IB
Ha senso a questo punto chiedersi quale sia la complessità dell’algoritmo C • A. Il prossimo teorema
risponde, sotto certe condizioni, a tale domanda.
Teorema 3.4.1 (Di trascodifica) Se A ha complessità polinomiale rispetto a size(IA ) e C ha complessità
polinomiale rispetto a size(IB ) allora C • A ha complessità polinomiale rispetto a size(IB ).
Dim. Poniamo per semplicità size(IA ) = r e size(IB ) = d. Poichè C ha complessità polinomiale,
esistono due costanti non negative, c1 e k1 , tali che, per d abbastanza grande, WC (d) ≤ c1 dk1 . Poichè A
ha complessità polinomiale, esistono due costanti non negative, c2 e k2 , tali che, per r abbastanza grande,
WA (r) ≤ c2 rk2 . Calcoliamo ora la dimensione r dell’istanza codificata come IA rispetto alla dimensione
dell’istanza di input IB . Poichè l’algoritmo di trascodifica utilizza al massimo c1 dk1 passi elementari per
tradurre e rappresentare l’istanza nel formato IA , la dimensione di quest’ultima istanza non può ecccedere
il numero di passi elementari e quindi r ≤ c1 dk1 .
Quindi la complessità nel caso peggiore WC•A (d) dell’algoritmo C • A (funzione della dimensione d
dell’input) soddisfa:
WC•A (d) = WC (d) + WA (r) ≤ c1 dk1 + c2 rk2 ≤ c1 dk1 + c1 c2 dk1 k2 ≤ cdk
con k = max(k1 , k1 k2 ) e c = c1 + c1 c2 .
3.5
Rappresentare istanze di grafi.
Sia dato un grafo orientato G(N, A), con associato un intero bv a ogni nodo v ∈ V e un intero cuv a ogni
arco uv ∈ A. Si vuole rappresentare il grafo e i parametri associati e calcolare la quantità di memoria
richiesta.
Matrice d’adiacenza. Un primo modo di rappresentazione è la cosiddetta matrice di adiacenza
(nodo-nodo): si tratta di una matrice binaria A di dimensione |N | × |N |, in cui l’elemento auv = 1 se
/ A.
l’arco uv ∈ A, mentre auv = 0 se uv ∈
Per semplicità ipotizziamo che ogni parametro richieda esattamente una cella di memoria per essere
rappresentato. Allora, occorreranno |N |2 celle per rappresentare la matrice A, |N |2 celle per rappresentare i parametri di arco [cuv ], e |N | celle per rappresentare i parametri di nodo [bv ]. In tutto, la
rappresantazione del grafo e dei suoi parametri richiede 2|N |2 + |N | celle.
25
c23
b3 v c34
3
b2 v2
b4
v4
c14
c21
b1
c41
v4
Figura 3.11: Esempio di grafo con parametri associati ai nodi e agli archi
v1
v2
v3
v4
v1
-
0
0
1
v2
1
-
1
v3
0
0
v4
1
0
v1
v2
v3
v4
v1
-
-
-
c14
0
v2
c21
-
c23
-
-
1
v3
-
-
-
c34
0
-
v4
c41
-
-
-
Figura 3.12: Matrice adiacenza nodi-nodi e parametri archi
Liste d’adiacenza. La seconda rappresentazione è quella cosiddetta a lista di adiacenza. Tutte le
informazioni relative agli archi (i parametri [cuv ] e le adiacenze) sono rappresentate da |N | liste (una per
nodo). La lista L(u), per u ∈ N , rappresenta gli archi della stella uscente di u. Il grafo di Figura 3.11
viene rappresentato come in Figura 3.13.
v1
b1
v2
b2
v3
b3
v4
b4
°
v4
c14
v1
c21
nil
°
v3
c23
nil
°
°
°
v4
v1
c34
c41
nil
nil
Figura 3.13: Liste di adiacenza
In questa rappresentazione, ogni arco uv ∈ A appare rappresentato esattamente una volta (l’indice del
nodo v appare nella lista L(u)) e quindi l’occupazione complessiva degli indici di nodo risulta pari a |A|
celle. Analogamente, il parametro cuv , per ogni uv ∈ A, apparirà una sola volta, e quindi l’occupazione
sarà di |A| celle. Inoltre, i puntatori agli archi occuperanno anch’essi esattamente |A| celle. Infine, i
parametri bv associati ai nodi richiedono |N | celle per essere rappresentati: analogamente, i puntantori
alle stelle uscenti occuperanno esattamente |N |. In tutto, l’occupazione sarà 3|A| + 2|N | celle.
Vogliamo ora rispondere alla domanda: quando è preferibile usare l’una o l’altra rappresentazione
26
per un grafo G(N, A)? Per semplicità, poniamo |N | = n e |A| = m, e supponiamo n ≤ m. Chiaramente
size(G(N, A)) = f (n, m), e in particolare, se usiamo la matrice di adiacenza avremo sizeA = 2n2 + n
mentre se usiamo le liste di adiacenza avremo sizeL = 3m + n. Si osservi ora che il numero di archi di
un grafo è limitato dal numero di possibili coppie di nodi e quindi m ≤ n2 . Di seguito, diremo che un
grafo è denso se m ≈ n2 , mentre sarà sparso se m ≈ n.
Figura 3.14: Grafo denso e grafo sparso
Quale rappresentazione? La rappresentazione da preferire dipende in genere dalle caratteristiche
del grafo e dal ”punto di vista”. Dal punto di vista dell’occupazione di memoria, si ha che sizeA =
2n2 + n = O(n2 ) mentre sizeL = 3m + 2n = O(m). Si ricordi che il numero massimo di archi di un grafo
).
orientato è pari a n(n − 1) (mentre il numero massimo di archi di un grafo non orientato è pari a n(n−1)
2
Quindi m ≤ n2 − n e di conseguenza m = O(n2 ). Quindi, ambedue le rappresentazioni richiedono (nel
caso peggiore) uno spazio O(n2 ) e si pone size(G) = O(n2 ). Di conseguenza, un algoritmo su grafi ha
Complessità Polinomiale se e solo se: W (size(G)) = W (n2 ) ≤ c1 (n2 )r con r costante e quindi (ponendo
k = 2r), si ha che un algoritmo su grafi ha Complessità Polinomiale se e solo se la sua Complessità nel
Caso Peggiore è O(nk ) con k costante.
Da un punto di vista pratico, la rappresentazione scelta può avere un effetto sull’efficienza nella
soluzione di istanze specifiche. In particolare, in caso di grafo denso m ≤ n2 , si avrà che le due rappresentazioni occupano una quantità di memoria confrontabile. Infatti: sizeA = O(m) = O(n2 ) = sizeL .
Di converso, nel caso di grafo sparso, le liste di adiacenza sono certamente convenienti. Ad esempio, se
n = 104 e m = 105 (grafo sparso), si ha sizeA ≈ 108 = 100 milioni di celle, mentre sizeL ≈ 105 ovvero
100.000 celle.
Analizziamo più in dettaglio qual è l’effetto di utilizzare l’una o l’altra delle due rappresentazioni dei
grafi sulla complessità degli algoritmi di soluzione.
Il lemma seguente fa uso di un importante risultato di trasformazione da una rappresentazione
all’altra. La dimostrazione, che viene omessa, è basata sulla costruzione di un opportuno algoritmo
di trasformazione.
Lemma 3.5.1 Sia G(N, A) un grafo orientato. Se A è la sua matrice di adiacenza, allora esiste un
algoritmo CAL che costruisce in tempo O(n2 ) la rappresentazione mediante liste di adiacenza. Di converso,
se L è la sua rappresentazione mediante liste di adiacenza, allora esiste un algoritmo CLA che costruisce
in tempo O(n2 ) la sua matrice di adiacenza.
Il precedente lemma dice che, dato un grafo orientato, è possibile passare efficientemente da una sua
rappresentazione all’altra. Questo risultato viene sfruttato nel prossimo teorema che spiega come, dal
punto di vista della efficienza teorica, le due rappresentazioni siano equivalenti.
27
Teorema 3.5.2 (Teorema dell’equivalenza delle rappresentazioni di un grafo) Sia dato un problema P
le cui istanze sono descritte mediante un grafo orientato. Un algoritmo A ha complessità polinomiale
rispetto a sizeA se e solo se esiste un algoritmo L che ha complessità polinomiale rispetto a sizeL .
Dim. La dimostrazione discende direttamente e banalmente dal Teorema 3.4.1 e dal Lemma 3.5.1.
3.6
Autovalutazione.
1. Dimostrare i vari enunciati del Teorema 3.2.2.
2. L’algoritmo A ha complessità pari 10n log n operazioni, mentre la complessità dell’algoritmo B è
n2 . Si determini il valore n0 per cui A è meglio di B per n ≥ n0 .
28