Você está na página 1de 183

Concetti di base di

complessit degli algoritmi

Problemi, algoritmi, programmi

Problema: il compito da svolgere


quali output vogliamo ottenere a fronte di certi input
cio quale funzione vogliamo realizzare
Algoritmo: i passi (il processo) da seguire per risolvere un
problema
un algoritmo prende gli input in ingresso ad un problema e
li trasforma in opportuni output
Come al solito, un problema pu essere risolto da tanti
algoritmi
Un algoritmo una sequenza di operazioni concrete
deve essere eseguibile da una macchina

Un algoritmo deve essere corretto


deve calcolare la funzione giusta
sappiamo che determinare la correttezza di un algoritmo
un problema indecidibile...
... questo per non vuole dire che non si possa fare niente
per cercare di capire se un algoritmo corretto o no

Un algoritmo pu essere descritto in diversi linguaggi


se usiamo un linguaggio di programmazione (C, C++, Java, C#,
ecc.) abbiamo un programma

Come linguaggio noi usiamo... lo pseudocodice


non un vero linguaggio di programmazione, ma ci assomiglia
molto
facile da tradurre in codice di un linguaggio di programmazione
quale C o Java (o Python)
Questanno faremo un esperimento fuori programma in
Javascript
il particolare linguaggio di programmazione con cui un algoritmo
implementato , dal punto di vista della complessit, un po'
come l'hardware: ci cambia solo le costanti moltiplicative

Primo esempio di problema/algoritmo

Problema: ordinamento
Input: una sequenza A di n numeri a1, a2, ... an
Output: una permutazione b1, b2, ... bn della sequenza di input
tale che b1 b2 ... bn

Algoritmo: insertion sort

INSERTION-SORT(A)
1 for j := 2 to A.length
2

key := A[j]

//Inserisce A[j] nella sequenza ordinata A[1..j1]

i := j 1

while i > 0 and A[i] > key

A[i + 1] := A[i]

i := i 1

A[i + 1] := key

pseudocodice

assegnamento: i := j
assegnamento multiplo: i := j := e
applicato da destra a sinistra
cio la stessa cosa che scrivere j := e; i := j

while, for, if-then-else come in C


// inizia un commento, che termina alla fine della riga
la struttura a blocchi data dalla indentazione

while i > 0 and A[i] > key

while (i > 0 and A[i] > key)


{

A[i + 1] := A[i]
i := i 1
A[i + 1] := key

A[i + 1] := A[i]

i := i 1
}
A[i + 1] := key

Le variabili sono locali alla procedura


Agli elementi degli array si accede come in C
A[j] l'elemento di indice j dell'array A
il primo elemento pu avere un indice diverso da 0

C' una nozione di sottoarray


A[i..j] il sottoarray che inizia dall'elemento i-esimo e
termina all'elemento j-esimo
e.g. A[1..5] il sottoarray con i primi 5 elemento dell'array A

pseudocodice (2)

Dati composti sono organizzati in oggetti


Gli oggetti hanno degli attributi (detti anche campi)
per indicare il valore di un attributo attr di un oggetto x,
scriviamo x.attr
gli array rappresentano dati composti, quindi sono oggetti
ogni array ha un attributo length, che contiene la lunghezza dell'array

A.length la lunghezza dell'array A

Una variabile che corrisponde ad un oggetto (es. un array) un


puntatore all'oggetto
molto simile ai puntatori in C e, sopratutto, al concetto di
reference in Java
per esempio, se abbiamo due variabili x and y, e x punta ad un
oggetto con un attributo f, dopo le seguenti istruzioni
y := x
x.f := 3
si ha che x.f = y.f = 3, in quanto, grazie all'assegnamento y :=
x, x e y puntano allo stesso oggetto

Un puntatore che non fa riferimento ad alcun oggetto ha valore


NIL

I parametri sono passati per valore


la procedura invocata riceve una copia dei parametri passati
se una procedura PROC ha un parametro x e dentro a PROC il
parametro x riceve il valore di y (x := y), la modifica non
visibile al di fuori della procedura (per esempio al chiamante)

Quando un oggetto passato come parametro, ci che viene


passato il puntatore all'oggetto
degli attributi non viene fatta una copia, e modifiche a questi
sono visibili al chiamante
se x un parametro che un oggetto con attributo f, gli effetti
dell'assegnamento x.f:=3 (il fatto che l'attributo f valga 3) sono
visibili al di fuori della procedura
questo il funzionamento di Java...

Modello di computazione

Quale la macchina sulla quale vengono eseguiti gli


algoritmi scritti in pseudocodice?
La macchina RAM!
Assunzione di base: ogni istruzione semplice di pseudocodice
tradotta in un numero finito di istruzioni RAM
per esempio x := y diventa, se ax e ay sono gli l'indirizzi in
memoria delle variabili x e y (ax e ay sono delle costanti):
LOAD
ay
STORE
ax

Da ora in poi adottiamo il criterio di costo costante


adatto per gli algoritmi che scriveremo, che non manipoleranno
mai numeri n richiederanno quantit di memoria molto pi
grandi della dimensione dei dati in ingresso

In conseguenza di ci abbiamo che ogni istruzione i di


pseudocodice viene eseguita in un tempo costante ci
Grazie a questa assunzione, da adesso in poi possiamo
dimenticarci che il modello computazionale dello
pseudocodice la macchina RAM
Inoltre, da ora in poi ci concentriamo sulla complessit
temporale, pi che su quella spaziale

Costo di esecuzione per INSERTION-SORT


INSERTION-SORT(A)

costo

1 for j := 2 to A.length

c1
c2

key := A[j]

//Inserisce A[j] nella sequenza A[1..j1]

i := j 1

while i > 0 and A[i] > key

A[i + 1] := A[i]

i := i 1

A[i + 1] := key

0
c4
c5
c6
c7
c8

numero
di volte
n
n-1
n-1
n-1

j= 2 t j
n

n-1

Note:
n = A.length = dimensione dei dati in ingresso
t2, t3 ... tn = numero di volte che la condizione del ciclo
while viene eseguita quando j = 2, 3, ... n

Tempo di esecuzione di INSERTION-SORT:


T n = c1n + c2 n 1+ c4 n 1+ c5 t j
+ c6 t j 1+ c7 t j 1+ c8 n 1

Se l'array A gi ordinato, t2 = ... = tn = 1


T(n) = an+b, cio T(n) = (n)
questo il caso ottimo
Se A ordinato, ma in ordine decrescente, t2=2, t3=3, ... tn=n
T(n) = an2+bn+c, cio T(n) = (n2)
questo il caso pessimo
7

Un classico problema: l'ordinamento

L'ordinamento degli elementi di una sequenza un esempio


classico di problema risolto mediante algoritmi
C' un gran numero di algoritmi di ordinamento disponibili:
insertion sort, bubblesort, quicksort, merge sort, counting sort,
...
Ne abbiamo appena visto uno di essi: insertion sort
Abbiamo visto che nel caso pessimo TINSERTION-SORT(n)
(n2)
possiamo anche scrivere che TINSERTION-SORT(n) = O(n2) (usando
la notazione O, senza specificare nel caso pessimo), in quanto
il limite superiore (che raggiunto nel caso pessimo) una
funzione in (n2)
anche TINSERTION-SORT(n)=(n), in quanto il limite inferiore
(raggiunto nel caso ottimo) (n)

Possiamo fare di meglio?


possiamo cio scrivere un algoritmo con un limite superiore
migliore?

S!

Merge sort

Idea dell'algoritmo:
se l'array da ordinare ha meno di 2 elementi, ordinato per
definizione
altrimenti:
si divide l'array in 2 sottoarray, ognuno con la met degli elementi di
quello originario
si ordinano i 2 sottoarray ri-applicando l'algoritmo
si fondono (merge) i 2 sottoarray (che ora sono ordinati)

MERGE-SORT un algoritmo ricorsivo

Un esempio di funzionamento:
42

16

28

36

26

78

84

42

16

28

36

26

78

84

42

16

28

36

26

78

84

16

42

28

36

26

78

84

16

28

36

42

26

78

84

16

26

28

36

42

78

84
9

pseudocodice di MERGE-SORT
MERGE-SORT(A, p, r)
1

if p < r

q := (p + r)/2

MERGE-SORT(A, p, q)

MERGE-SORT(A, q+1, r)

MERGE(A, p, q, r)

Per ordinare un array A = A[1], A[2], ... A[n] invochiamo


MERGE-SORT(A, 1, A.length)

MERGE-SORT adotta una tecnica algoritmica classica: divide


et mpera
Se il problema da risolvere grosso:

dividilo in problemi pi piccoli della stessa natura


risolvi (domina) i problemi pi piccoli
combina le soluzioni

Dopo un po' che dividiamo il problema in altri pi piccoli, ad


un certo punto arriviamo ad ottenere problemi piccoli a
sufficienza per poterli risolvere senza dividerli ulteriormente
una tecnica naturalmente ricorsiva in quanto, per risolvere i
problemi pi piccoli, applichiamo lo stesso algoritmo del
problema pi grosso

Per completare l'algoritmo dobbiamo definire un sottoalgoritmo MERGE che "combina" le soluzioni dei problemi pi
piccoli

10

Fusione (merge) di sottoarray ordinati

Definizione del problema (input/output)


Input: 2 array ordinati A[p..q] e A[q+1..r] di un array A
Output: l'array ordinato A[p..r] ottenuto dalla fusione degli
elementi dei 2 array iniziali

Idea dell'algoritmo:

1. si va all'inizio dei 2 sottoarray


2. si prende il minimo dei 2 elementi correnti
3. si inserisce tale minimo allinizio dell'array da restituire
4. si avanza di uno nell'array da cui si preso il minimo
5. si ripete dal passo 2

pseudocodice:
MERGE (A, p, q, r)
1 n1 := q p + 1
2 n2 := r q
3 crea (alloca) 2 nuovi array L[1..n1+1] e R[1..n2+1]
4 for i := 1 to n1
5
L[i] := A[p + i - 1]
6 for j := 1 to n2
7
R[j] := A[q + j]
8 L[n1 + 1] :=
9 R[n2 + 1] :=
10 i := 1
11 j := 1
12 for k := p to r
13
if L[i] R[j]
14
A[k] := L[i]
15
i := i + 1
16
else A[k] := R[j]
17
j := j + 1

11

Analisi dell'algoritmo MERGE

Nell'algoritmo MERGE prima si copiano gli elementi dei 2


sottoarray A[p..q] e A[q+1..r] in 2 array temporanei L e R,
quindi si fondono L e R in A[p..r]
Escamotage: per non dover controllare se L e R sono vuoti si
usa una sentinella, un valore particolare (), pi grande di
ogni possibile valore, messo in fondo agli array (linee 8-9)
Dimensione dei dati in input: n = r p + 1
L'algoritmo fatto di 3 cicli for:
2 cicli di inizializzazione (l. 4-7), per assegnare i valori a L e R
il primo eseguito n1 volte, il secondo n2 volte, con
(n1) = (q-p+1) = (n/2) = (n)
(n2) = (r-q) = (n/2) = (n)

si poteva giungere allo stesso risultato notando che n1 + n2 = n, quindi


(n1 + n2) = (n)

Il ciclo principale (l. 12-17) eseguito n volte, e ogni linea ha


costo costante
In totale TMERGE(n) = (n)
MERGE (A, p, q, r)
costo
1 n1 := q p + 1
c
2 n2 := r q
c
3 //crea 2 nuovi array L[1..n1+1] e R[1..n2+1] (n)
4 for i := 1 to n1
(n1) per tutto il ciclo
5
L[i] := A[p + i - 1]
6 for j := 1 to n2
(n2) = (n/2) = (n)
7
R[j] := A[q + j]
8 L[n1 + 1] :=
c
9 R[n2 + 1] :=
c
10 i := 1
c
11 j := 1
c
12 for k := p to r
(n) per il ciclo
13
if L[i] R[j]
c
14
A[k] := L[i]
c
15
i := i + 1
c
16
else A[k] := R[j]
c
17
j := j + 1
c

12

Pi in generale:
Complessit di un algoritmo divide et impera

In generale, un algoritmo divide et impera ha le caratteristiche


seguenti:
si divide il problema in sottoproblemi, ognuno di dimensione 1/b
di quello originale
se il sottoproblema ha dimensione n piccola a sufficienza (n<c,
con c una costante caratteristica del problema), esso pu essere
risolto in tempo costante (cio (1))
indichiamo con D(n) il costo di dividere il problema, e C(n) il
costo di ricombinare le soluzioni dei sottoproblemi
T(n) il costo per risolvere il problema totale

Possiamo esrpimere il costo T(n) tramite la seguente


equazione di ricorrenza (o ricorrenza):
Tn = 1
se n < c

Dn + aTn / b + Cn altrimenti

Ricorrenza per l'algoritmo MERGE-SORT:


a = b = c = 2, D(n) = (1), C(n) = (n)
Tn = 1
se n < 2

2Tn / 2+ n altrimenti

in realt dovrebbe essere T(n/2) + T(n/2) invece di 2T(n/2),


ma l'approssimazione non influisce sul comportamento asintotico
della funzione T(n)

Come risolviamo le ricorrenze?


Vedremo tra poco...
... per ora:

13

Complessit di MERGE-SORT

Riscriviamo la ricorrenza di MERGE-SORT:


Tn = c
se n < 2

2Tn / 2+ cn altrimenti

Possiamo disegnare l'albero di ricorsione (consideriamo per


semplicit il caso in cui la lunghezza n dell'array una potenza
di 2)
cn
cn
cn/2

log 2 n

cn/4

cn

cn/2

cn/4

cn/4

cn/4

c c c c c c c c c c c c c

cn

cn

Totale: cnlogn+ cn

Sommando i costi dei vari livelli otteniamo


T(n) = cn log(n) + cn, cio TMERGE-SORT(n) = (n log(n))

14

Un inciso: MERGE-SORT non ricorsivo

La complessit spaziale della versione ricorsiva di MERGESORT TMERGE-SORT(n) = (n log(n))


Perch?
(Come tutti gli algoritmi) MERGE-SORT pu essere
codificato anche in versione non ricorsiva:

42

16

28

36

26

78

84

F1

16

42

28

36

26

78

84

F2

16

28

36

42

26

78

84

F1

16

26

28

36

42

78

84

F2

La complessit spaziale della versione


ricorsiva di MERGE-SORT SMERGE-SORT(n)
= (n log(n))
La complessit spaziale della versione non
ricorsiva di MERGE-SORT
SMERGE-SORT(n) = (n)
La codifica della versione non ricorsiva di
MERGE-SORT un po pi
complicata

15

Risoluzione di ricorrenze

Tre tecniche principali:


sostituzione
albero di ricorsione
teorema dell'esperto (master theorem)

Metodo della sostituzione:


formulare un'ipotesi di soluzione
sostituire la soluzione nella ricorrenza, e dimostrare (per
induzione) che in effetti una soluzione

Esempio: cerchiamo un limite superiore per la seguente T(n):


T(n) = 2T(n/2) + n
supponiamo T(n) = O(n log2(n))
dobbiamo mostrare che T(n) cn log2(n) per una opportuna
costante c>0
supponiamo che ci valga per T(n/2), cio
T(n/2) cn/2 log2(n/2)
allora, sostituendo in T(n) abbiamo
T(n) 2cn/2 log2(n/2) + n cn log2(n/2) + n =
= cn log2(n) -cn log2(2) + n = cn log2(n) -cn + n cn log2(n)
basta che c 1

dobbiamo per mostrare che la disuguaglianza vale per n = 1


(condizione al contorno); supponiamo che sia T(1) = 1, allora
T(1) =1 c1 log2(1) = 0? No!
per T(n) cn log2(n) deve valere solo da un certo n0 in poi, che
possiamo scegliere arbitrariamente; prendiamo n0 = 2, e notiamo
che, se T(1) = 1, allora, dalla ricorrenza, T(2) = 4 e T(3) = 5
inoltre, per n > 3 la ricorrenza non dipende pi dal problematico T(1)

ci basta determinare una costante c tale che T(2) = 4 c2 log2(2)


e T(3) = 5 c3 log2(3)
per ci basta prendere c 2

16

Osservazioni sul metodo di sostituzione (1)


Consideriamo il seguente caso:
T(n) = T(n/2)+T(n/2)+1
Proviamo a vedere se T(n) = O(n): ipotizziamo T(n) c.n:
T(n) cn/2+cn/2+1 = cn+1
basta prendere c=1 e siamo a posto?
No! perch non abbiamo dimostrato la forma esatta della
disuguaglianza!
Dallipotesi T(n/2) c (n/2) abbiamo dedotto solo
T(n) c (n) +1:
Linduzione non dimostrata!
Potremmo prendere un limite pi alto, e dimostrare che T(n)
O(n2) (cosa che vera), ma in effetti si pu anche dimostrare
che T(n) = O(n), dobbiamo solo avere un'accortezza:
Mostriamo che T(n) cn-b, con b un'opportuna costante
se fosse cos, allora T(n)= O(n)
T(n) cn/2-b+cn/2-b+1 = cn-2b+1 cn - b
basta prendere b 1
Attenzione per:
T(n) = 2T(n/2) + n O(n)?
Ipotesi: T(n) cn; ricavo
T(n) cn + n = O(n), quindi C.V.D.?
No!
Dobbiamo mostrare la forma esatta della disuguaglianza, e
(c+1)n non cn
17

Osservazioni sul metodo di sostituzione (2)

Altro esempio: T n = 2T n + log 2 n


poniamo m = log2(n), quindi n = 2m, otteniamo
T(2m) = 2T(2m/2)+m
Ponendo S(m) = T(2m) abbiamo
S(m) = 2S(m/2) + m
quindi S(m) = O(m log2(m))
Quindi, sostituendo all'indietro:
T(n) = O(log2(n) log2log2(n))

18

Metodo dell'albero di ricorsione

Un metodo non molto preciso, ma utile per fare una congettura


da verificare poi con il metodo di sostituzione
Idea: a partire dalla ricorrenza, sviluppiamo l'albero delle
chiamate, indicando per ogni chiamata la sua complessit
Esempio: T(n) = T(n/3) + T(2n/3)+O(n)
Prima chiamata:

cn

T n / 3
Espandiamo:

T 2n / 3
cn

cn /3

T n / 9

2cn/3

T 2n / 9

T 2n / 9 T 4n / 9

fino in fondo:

cn

cn

cn /3

log 3 / 2 n

cn/9

cn

2cn/3

2cn/9

2cn/9

4cn/9

cn

Se l'albero fosse completo, sommando i costi livello per livello, a


ogni livello avremmo un costo cn, ed il numero di livelli k
sarebbe tale che n(2/3)k=1, cio k = log3/2n ...
19

Albero di ricorsione (2)


... per l'albero non completo
il ramo pi a destra s tale che alla fine n(2/3)k=1, ma quello pi a
sinistra tale che n(1/3)k'=1, cio k' = log3n
non fa niente, per ora ci accontentiamo di una congettura anche abbastanza
grossolana, tanto poi la andiamo a controllare con il metodo di sostituzione
In definitiva, abbiamo la congettura che T(n)=O(n log2n)
verifichiamola, mostrando che T(n) dn log2n:
T(n) (d/3)n log2(n/3)+(2d/3)n log2(2n/3)+cn
= (d/3)n log2n (d/3)n log23+ (2d/3)n log2n (2/3)dn log23 +(2d/3)n+cn
= dn log2n -dn log23+(2d/3)n+cn
= dn log2n -dn(log23-2/3)+cn
dn log2n
se d>c/(log23-2/3)

20

Albero di ricorsione (3)

Congetturiamo (e verifichiamo) anche che T(n)=(n log2n), cio che T(n)


kn log2n
T(n) (k/3)n log2(n/3)+(2k/3)n log2(2n/3)+cn
= kn log2n -kn log23+(2k/3)n+cn
= kn log2n -kn(log23-2/3)+cn
kn log2n
se 0<k<c/(log23-2/3)
Notiamo che in entrambe le sostituzioni sopra c' anche da considerare il
caso base per determinare se opportuni d e k esistono
lasciato per casa come esercizio...
Quindi in effetti T(n)=(n log2n)

21

Teorema dell'esperto (Master Theorem)

Data la ricorrenza:
T(n) = aT(n/b) + f(n)

(in cui a 1, b >1, e n/b o n/b o n/b)


1. se f(n) = O(nlogba-) per qualche >0, allora T(n) = (nlogba)
2. se f(n) = (nlogba), allora T(n) = (nlogbalog(n))
3. se f(n) = (nlogba+) per qualche >0, e af(n/b) cf(n) per qualche
c < 1 e per tutti gli n grandi a sufficienza, allora T(n) = (f(n))

Alcune osservazioni...
La soluzione data dal pi grande tra nlogba e f(n)
se nlogba il pi grande, T(n) (nlogba)
se f(n) il pi grande, T(n) (f(n))
se sono nella stessa classe secondo la relazione , T(n)
(f(n)log(n))

Pi grande o pi piccolo in effetti "polinomialmente pi


grande" e "polinomialmente pi piccolo"
n polinomialmente pi piccolo di n2
n log(n) polinomialmente pi grande di n

Il teorema dell'esperto non copre tutti i casi!


se una delle due funzioni pi grande, ma non polinomialmente
pi grande...
n log(n) pi grande di n, ma non polinomialmente pi grande

Applichiamo il teorema dell'esperto a MERGE-SORT:


T(n) = 2T(n/2) + (n)
a=b=2
f(n) = n
nlogba = n1 = n

siamo nel caso 2: TMERGE-SORT(n) = (n log(n))


22

Un caso particolare, ed un ulteriore risultato

Notiamo che l'enunciato del teorema dell'esperto si semplifica


un po' se f(n) una funzione (nk), con k una qualche costante:

1. se k < logba, allora T(n) = (nlogba)


2. se k = logba, allora T(n) = (nklog(n))
3. se k > logba, allora T(n) = (nk)
nel caso 3 la condizione aggiuntiva automaticamente verificata
(a/bk) < 1

Un altro teorema utile per risolvere certi tipi di ricorrenze:


Data la ricorrenza (in cui i coefficienti ai sono interi 0)

(1)

T (n) a T (n i ) cn k

1ih
a=

se n m h
se n m

ai

1 i h

in cui poniamo
allora abbiamo che:
1. se a=1, allora T(n)=O(nk+1)
2. se a 2, allora T(n)=O(annk)

Per esempio, data la ricorrenza T(n) = T(n-1) + (n),


otteniamo che T(n)=O(n2)
questa la ricorrenza che otterremmo con una versione
ricorsiva di INSERTION-SORT

23

Grafi (richiamo)

Un grafo una coppia (V, E) in cui V un insieme finito di


nodi (detti anche vertici), e E VV una relazione binaria su
V che rappresenta gli archi del grafo
se u e v sono nodi del grafo, la coppia (u,v) un arco, ed
rappresentata graficamente come:
u

in questo caso l'arco orientato, in quanto c' un ordine tra i


vertici, prima u, poi v
se non c' un ordine tra i nodi (che quindi sono solo un insieme,
{u,v} allora diciamo che l'arco non orientato:
u

Un grafo orientato se i suoi archi lo sono, non orientato


altrimenti
esempio di grafo non orientato:

Un cammino una sequenza di nodi [v0, v1, v2, vn] tali che
tra ogni coppia di nodi della sequenza (vi, vi+1) c' un arco
i nodi v0, vn appartengono al cammino
la lunghezza del cammino data da n (numero di vertici -1)

In un grafo non orientato, il cammino forma un ciclo se v0=vn,


e contiene almeno 3 nodi (cio se ha almeno 3 archi)
Un grafo che non ha cicli aciclico

Un grafo non orientato connesso se tra ogni coppia di vertici


esiste un cammino

24

Alberi (richiamo)

Un albero un grafo connesso, aciclico, non orientato


un albero radicato se un nodo viene indicato come la radice

radice
profondit 0

nodi interni
profondit 1
altezza = 3
profondit 2

profondit 3

foglie

Ogni nodo dell'albero raggiungibile dalla radice tramite un


cammino (che unico, in quanto il grafo aciclico)

Chiamiamo:

foglie: gli ultimi nodi dei cammini dalla radice


nodi interni: tutti i nodi dei cammini tra la radice e le foglie
profondit (di un nodo N): la distanza di N dalla radice
altezza (dell'albero): la ditsnza massima tra la radice e una foglia
antenato (di un nodo N): ogni nodo che precede N sul cammino
dalla radice a N
padre (di un nodo N): il nodo che immediatamente precede N
lungo il cammino dalla radice a N
figlio (di un nodo N): ogni nodo di cui N padre
fratelli (di un nodo N): i nodi che hanno lo stesso padre di N

Un albero binario se ogni nodo ha al pi 2 figli

25

HEAPSORT

MERGE-SORT efficiente dal punto di vista del tempo di


esecuzione, ma non ottimale dal punto di vista dell'uso della
memoria (a meno di non usare la versione non ricorsiva):
ogni MERGE richiede di allocare 2 array, di lunghezza (n)
usa una quantit di memoria aggiuntiva rispetto all'array da
ordinare che non costante, cio non ordina sul posto

HEAPSORT, invece, non solo efficiente (ordina in tempo


(n log(n))), ma ordina sul posto
L'idea alla base di HEAPSORT che un array pu essere visto
come un albero binario:
A[1] la radice
per ogni elemento A[i], A[2i] e A[2i+1] sono i suoi figli, e
A[i/2] il padre

Esempio:
1

10 11 12

a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12


1

a8
8

a4

a1

a2

a5

a6

a9

a10

a11

10

11

a3
7

a7

a12
12

26

Gli heap (mucchi)

Uno heap binario un albero binario quasi completo


quasi completo = tutti i livelli sono completi, tranne al pi
l'ultimo, che potrebbe essere completo solo fino a un certo punto
da sinistra
l'albero binario che deriva dall'interpretazione di un array come
albero quasi completo

Un max-heap uno heap tale che, per ogni nodo x dell'albero,


il valore contenuto nel padre di x del contenuto di x
usando la corrispondenza albero-heap, questo vuole dire che
A[i/2]A[i]

Esempio:
1

10 11 12

4
8

8
5

10

11

2
12

Si noti che in un max-heap l'elemento massimo nella radice


dove il minimo?

27

Alcune operazioni sugli heap

Operazioni di base:
PARENT(i)
1 return i/2
LEFT(i)
1 return 2*i
RIGHT(i)
1 return 2*i + 1

Quindi, in un max-heap abbiamo che A[PARENT(i)] A[i]


esistono anche i min-heap, per le quali A[PARENT(i)] A[i]

Per realizzare l'ordinamento usiamo i max-heap

Ogni array A che rappresenta uno heap ha 2 attributi:


A.length, che rappresenta il numero totale di
elementi dell'array
A.heap-size, che rappresenta il numero di elementi
dello heap
A.heap-size A.length, e solo gli elementi fino a
A.heap-size hanno la propriet dello heap
per l'array potrebbe contenere elementi
dopo l'indice A.heap-size, se A.heapsize<A.length
NB: Per A.length il numero effettivo n di
elementi da ordinare, non corrisponde
necessariamente a una potenza di 2 (-1)
28

Algoritmi di supporto

Un algoritmo che, dato un elemento di un array tale che i suoi


figli sinistro e destro siano dei max-heap, ma in cui A[i] (la
radice del sottoalbero) potrebbe essere < dei suoi figli,
modifica l'array in modo che tutto l'albero di radice A[i] sia un
max-heap
MAX-HEAPIFY(A, i)
1 l := LEFT(i)
2 r := RIGHT(i)
3
4
5

if l A.heap-size and A[l] > A[i]


max := l
else max := i

6
7

if r A.heap-size and A[r] > A[max]


max := r

8 if max i then
9
swap A[i] A[max]
10
MAX-HEAPIFY(A, max)

TMAX-HEAPIFY = O(h), dove h l'altezza dell'albero, che


O(log(n)), poich l'albero quasi completo
quindi, TMAX-HEAPIFY = O(log(n))

29

Osservazione

Questo si sarebbe anche potuto mostrare usando il teorema


dell'esperto per la seguente ricorrenza, che rappresenta il
tempo di esecuzione di MAX-HEAPIFY nel caso pessimo:
T(n) = T(2n/3) + (1)
nel caso pessimo l'ultimo livello dell'albero esattamente pieno a
met, e l'algoritmo viene applicato ricorsivamente sul sottoalbero
sinistro:

20

21
22

23
24

30

Da array a heap (1)


Un algoritmo per costruire un max-heap a
partire da un array
idea: costruiamo il max-heap bottom-up,
dalle foglie, fino ad arrivare alla radice
osservazione fondamentale: tutti gli
elementi dall'indice A.length/2 in
poi sono delle foglie, quelli prima sono
dei nodi interni
i sottoalberi fatti solo di foglie sono,
presi singolarmente, gi degli heap, in
quanto sono fatti ciascuno di un unico
elemento

31

Da array a heap (2)


BUILD-MAX-HEAP(A)
1 A.heap-size := A.length
//heap-size viene inizializzata a n
2 for i := A.length/2 downto 1
3
MAX-HEAPIFY(A, i)

Costo di BUILD-MAX-HEAP?
ad occhio, ogni chiamata a MAX-HEAPIFY costa
O(log(n)), e vengono fatte n/2 chiamate (con n che
A.length), quindi il costo O(n log(n))
ma in realt questo limite non stretto...
Osserviamo che:
l'altezza di un albero quasi completo di n nodi log2(n)
se definiamo come altezza di un nodo di uno heap la
lunghezza del cammino pi lungo che porta ad una foglia,
il costo di MAX-HEAPIFY invocato su un nodo di altezza
h O(h)
il numero massimo di nodi di altezza h di uno heap
n/2h+1
Quindi MAX-HEAPIFY viene invocato n/2h+1 volte ad ogni
altezza h, quindi il costo di BUILD-MAX-HEAP
lgn h

2h+1 O(h) = O n 2h

h=0
h=0

lgn

cio O(n), in quanto noto che

h
1/ 2
=
=2

h
2
(1 1/ 2 )
h=0 2

32

Infatti:

1
x =

( 1 x)
h=0
h

h
1

d x =

(
1

x)
h=0
hx h1 = 1

2
dx
( 1 x)
h=0

x
h
hx =

2
( 1 x)
h=0

h
1/ 2
=
=2

h
2
(1 1/ 2 )
h=0 2
Oppure: esercizio (per induzione)

h
n2
= 2 n

h
2
h=1 2
n

33

HEAPSORT

Possiamo a questo punto scrivere l'algoritmo di HEAPSORT:


HEAPSORT(A)
1 BUILD-MAX-HEAP(A)
2 for i := A.length downto 2
3
swap A[1] A[i]
4
A.heap-size := A.heap-size 1
5
MAX-HEAPIFY(A,1)
idea: a ogni ciclo piazziamo l'elemento pi grande (che
il primo dell'array, in quanto questo un max-heap) in
fondo alla parte di array ancora da ordinare (che quella
corrispondente allo heap)
fatto ci, lo heap si decrementa di 1, e si ricostruisce
il max-heap mettendo come radice l'ultima foglia a
destra dell'ultimo livello, e invocando MAXHEAPIFY

La complessit di HEAPSORT O(n log(n)), in quanto


BUILD-MAX-HEAP ha costo O(n)
MAX-HEAPIFY invocato n volte, e ogni sua chiamata
ha costo O(log(n))

34

QUICKSORT

QUICKSORT un algoritimo in stile divide-et-impera


ordina sul posto

Nel caso pessimo (vedremo) ha complessit (n2)


Per in media funziona molto bene (in media ha complessit
(n log(n)))
inoltre ha ottime costanti

Idea di base del QUICKSORT: dato un sottoarray A[p..r] da


ordinare:
(dividi) riorganizza A[p..r] in 2 sottoarray A[p..q-1] e A[q+1..r]
tali che tutti gli elementi di A[p..q-1] siano A[q] e tutti gli
elementi di A[q+1..r] siano A[q]
(impera) ordina i sottoarray A[p..q-1] e A[q+1..r] riutilizzando
QUICKSORT
(combina) nulla! L'array A[p..r] gi ordinato
QUICKSORT(A, p, r)
1 if p < r
2
q := PARTITION(A, p, r)
3
QUICKSORT(A, p, q1)
4
QUICKSORT(A, q+1, r)

Per ordinare un array A: QUICKSORT(A,1,A.length)

35

PARTITION

La cosa difficile di QUICKSORT partizionare l'array in 2


parti:
PARTITION(A, p, r)
1 x := A[r]
2 i := p 1
3 for j := p to r 1
4
if A[j] x
5
i := i + 1
6
swap A[i] A[j]
7 swap A[i+1] A[r]
8 return i + 1
l'elemento x (cio A[r] in questa implementazione) il pivot
Scelta (a priori) nondeterministica

Complessit di PARTITION: (n), con n = r-p+1


i

i=j

p
1

p
1

p
1

j
p
1

8
36

Complessit di QUICKSORT (1)


Il tempo di esecuzione di QUICKSORT
dipende da come viene partizionato l'array
Se ogni volta uno dei 2 sottoarray vuoto e
l'altro contiene n-1 elementi si ha il caso
pessimo
la ricorrenza in questo caso :
T(n) = T(n-1) + (n)
abbiamo visto che la soluzione di questa
ricorrenza O(n2)
si pu anche dimostrare (per esempio
per sostituzione) che anche (n2)
un caso in cui si ha sempre questa
situazione completamente sbilanciata
quando l'array gi ordinato

37

Complessit di QUICKSORT (2)


Nel caso ottimo, invece, i 2 array in cui il
problema viene suddiviso hanno esattamente la
stessa dimensione n/2
la ricorrenza in questo caso :
T(n) = 2T(n/2) + (n)
la stessa ricorrenza di MERGE-SORT, ed
ha quindi la stessa soluzione (n log(n))
Notiamo che se la proporzione di divisione,
invece che essere n/2 ed n/2, fosse n/10 e
9n/10, comunque la complessit sarebbe (n
log(n))
solo, la costante nascosta dalla notazione
sarebbe pi grande
abbiamo gi visto qualcosa di molto simile
per la suddivisione n/3 e 2n/3

38

QUICKSORT nel caso medio (solo intuizione)


In media ci va un po' bene ed un po' male
bene = partizione ben bilanciata
male = partizione molto sbilanciata
Qualche semplificazione:
ci va una volta bene ed una volta male
quando va bene => ottimo
n/2 e n/2
quando va male => pessimo
n-1 e 0

(n)

n
0

n-1

(n-1)/2 - 1

(n-1)/2

39

QUICKSORT nel caso medio (solo intuizione)

Albero di ricorsione in questo caso (ogni


divisione costa n):
(n)

n
0

n-1

(n-1)/2 - 1

(n-1)/2

costo di una divisione cattiva + una divisione


buona = (n)
lo stesso costo di una singola divisione
buona
dopo una coppia divisione cattiva divisione
buona il risultato una divisione buona
quindi alla fine il costo di una coppia cattiva
buona lo stesso di una divisione buona, ed il
costo di una catena di tali divisioni la stessa...
laltezza dellalbero 2.log(n) invece di log(n)
... quindi (n log(n))
le costanti moltiplicative peggiorano un po', ma
l'ordine di grandezza non cambia!

40

QUICKSORT nel caso medio (in generale .. )


Assumendo che ogni ripartizione posizioni il
pivot con uguale probabilit in ogni posizione:
1 n 1
T (n) cn T (k ) T (n 1 k ),
n k 0
T (0) T (1) c

Che, con un po di pazienza, si pu dimostrare


essere (n.log(n))

41

Limite inferiore per l'ordinamento (1)


E' stato dimostrato che l'ordinamento basato su
confronto (come INSERTION-SORT, MERGE-SORT,
o HEAPSORT, per esempio) deve fare (n log(n))
confronti nel caso pessimo:
Idea della dimostrazione: ogni computazione di un
qualsiasi algoritmo parte da un array e produce un
risultato che una permutazione dellarray originario.
Tutte le possibili computazioni devono poter produrre
tutte le possibili permutazioni:
Qualsiasi algoritmo deve avere almeno n! diverse
possibili computazioni: se le rappresentiamo mediante
un albero:

a1 , a2 ,

an
a1 >= a2

a1 < a2

a5 , a8 ,

a23

a12, a21,

a6
42

Limite inferiore per l'ordinamento (2)


Lunghezza della computazione massima = altezza
dellalbero.
Laltezza minima: quando lalbero bilanciato:
(log n): ogni algoritmo (log n!).
Qual lordine di grandezza di log n!
?

n n
n! n(n 1)(n 2)
2 2
e quindi ,

n n
log( n!) log
2 2

n
2

che (nlog n).

Daltra parte, log( n!) log n log( n 1)


cosicch log(n!) nlog(n).
questo risultato ha come conseguenza che un algoritmo
come MERGE-SORT (o HEAPSORT) ottimale nel
caso pessimo
tuttavia, questo non significa che l'ordinamento un
caso chiuso, che abbiamo trovato la soluzione ideale,
o che dobbiamo sempre usare MERGE-SORT o
HEAPSORT
in effetti, alla fin fine l'algoritmo di ordinamento
forse pi usato QUICKSORT, che non ottimale
nel caso pessimo...
inoltre, possiamo in effetti fare meglio di MERGE-SORT
e HEAPSORT!
per dobbiamo evitare di fare confronti
43

COUNTING-SORT

Ipotesi fondamentale: i valori da ordinare non sono pi grandi


di una certa costante k
Idea di base: se nell'array ci sono me valori pi piccoli di un
certo elemento e (il cui valore ve) nell'array ordinato
l'elemento e sar in posizione me+1

quindi, basta contare quante "copie" dello stesso valore ve sono


contenute nell'array
usiamo questa informazione per determinare, per ogni elemento e
(con valore ve tale che 0 ve k), quanti elementi ci sono pi
piccoli di e
dobbiamo anche tenere conto del fatto che nell'array ci possono
essere elementi ripetuti
es. 2, 7, 2, 5, 1, 1, 9

pseudocodice
parametri: A l'array di input (disordinato), B conterr gli
elementi ordinati (cio l'output), e k il massimo tra i valori di
A
A e B devono essere della stessa lunghezza n
COUNTING-SORT (A, B, k)
1 for i := 0 to k
2
C[i] := 0
3 for j := 1 to A.length
4
C[A[j]] := C[A[j]] + 1
5 //C[i] ora contiene il numero di elementi uguali a i
6 for i := 1 to k
7
C[i] := C[i] + C[i - 1]
8 //C[i] ora contiene il numero di elementi i
9 for j := A.length downto 1
10
B[C[A[j]]] := A[j]
11
C[A[j]] := C[A[j]] - 1

44

esempio di COUNTING-SORT e complessit

Se A = 2,5,3,0,2,3,0,3
A.length = 8
B deve avere lunghezza 8

Se eseguiamo COUNTING-SORT(A, B, 5)
prima di eseguire la linea 5 (cio alla fine del loop 3-4)
C = 2,0,2,3,0,1
prima di eseguire la linea 8 C = 2,2,4,7,7,8
dopo le prime 3 iterazioni del ciclo 9-11 abbiamo

1. B = _,_,_,_,_,_,3,_ , C = 2,2,4,6,7,8
2. B = _,0,_,_,_,_,3,_ , C = 1,2,4,6,7,8
3. B = _,0,_,_,_,3,3,_ , C = 1,2,4,5,7,8

alla fine dell'algoritmo


B = 0,0,2,2,3,3,3,5 , C = 0,2,2,4,7,7

La complessit di COUNTING-SORT data dai 4 cicli for:

il ciclo for delle linee 1-2 ha complessit (k)


il ciclo for delle linee 3-4 ha complessit (n)
il ciclo for delle linee 6-7 ha complessit (k)
il ciclo for delle linee 9-11 ha complessit (n)

La complessit globale (n + k)
Se k O(n), allora il tempo di esecuzione O(n)
lineare!

COUNTING-SORT "pi veloce" (cio ha complessit


inferiore) di MERGE-SORT e HEAPSORT (se k O(n)) perch
fa delle assunzioni sulla distribuzione dei valore da ordinare
(assume che siano tutti k)
sfrutta l'assunzione: veloce se k O(n), altrimenti ha
complessita maggiore (anche di molto) di MERGE-SORT e
HEAPSORT
45

Strutture Dati

46

Scopo delle strutture dati


Le strutture dati sono aggeggi usati per contenere
oggetti
rappresentano collezioni di oggetti
spesso (ma non sempre) gli oggetti di in una
struttura dati hanno una chiave, che serve per
indicizzare l'oggetto, e dei dati satelliti associati
(che sono i dati di interesse che porta con s
l'oggetto)
per esempio, si fa ricerca sulla chiave per
accedere ai dati satelliti, che possono essere
qualunque
Ci sono 2 tipi di operazioni sulle strutture dati:
operazioni che modificano la collezione
operazioni che interrogano la collezione

47

Alcune operazioni tipiche sulle strutture dati:


SEARCH(S, k)
restituisce l'oggetto (o, meglio il suo riferimento) x nella
collezione S con chiave k, NIL se nessun oggetto nella
collezione ha chiave k
un'operazione di interrogazione

INSERT(S, x)
inserisce l'oggetto x nella collezione S
un'operazione che modifica la collezione

DELETE(S, x)
cancella l'oggetto x dalla collezione S (op. di modifica)

MINIMUM(S)
restituisce l'oggetto nella collezione con la chiave pi
piccola (op. di interrogazione)

MAXIMUM(S)
restituisce l'oggetto nella collezione con la chiave pi
grande (op. di interrogazione)

SUCCESSOR(S, x)
restituisce l'oggetto che segue x nella collezione, secondo
una qualche relazione di ordinamento (op. di
interrogazione)
per esempio, potrebbe essere l'elemento con la prossima
chiave pi grande, se c' un ordinamento sulle chiavi
potrebbe essere qualcosa d'altro (la sua definizione
dipende dalla specifica struttura dati)

PREDECESSOR(S,x)
restituisce l'oggetto che precede x nella collezione, secondo
una qualche relazione di ordinamento (op. di
interrogazione)

48

Pile (Stack)

Cominciamo con un esempio semplicissimo di struttura dati:


la pila
Ad un livello astratto, una pila una collezione di oggetti sulla
quale possiamo fare le seguenti operazioni:
controllare se vuota
inserire un elemento nella collezione (PUSH)
cancellare un elemento dalla collezione (POP)
l'operazione di POP restituisce l'elemento cancellato

Una pila gestita con una politica LIFO (Last In First Out)
l'elemento che viene cancellato (pop) quello che stato inserito
per ultimo (cio quello che nella pila da meno tempo)
cio, se viene fatta una PUSH di un oggetto e su una pila S, seguita
immediatamente da una POP su S, l'elemento restituite dalla POP lo
stesso e di cui era stata fatta la PUSH

Se la pila pu contenere al massimo n elementi, possiamo


implementarla come un array di lunghezza n
per tenere traccia dell'indice dell'elemento che stato inserito per
ultimo viene introdotto un attributo, chiamato top
cio, se una pila S implementata mediante un array, S.top l'indice
dell'ultimo elemento inserito

se S.top = t, allora S[1], S[2], ... S[t] contengono tutti gli elementi, e S[1]
stato inserito prima di S[2], che stato inserito prima di S[3], ecc.

se S.top = 0, la pila vuota, e nessun elemento pu essere cancellato


se S.top = S.length = n, la pila piena, e nessun elemento vi pu
essere aggiunto

49

pseudocodice per le operazioni sulle pile

Se una pila implementata tramite un array, lo pesudocodice


per le operazioni su di essa sono le seguenti:
STACK-EMPTY(S)
1 if S.top = 0
2
return TRUE
3 else return FALSE
PUSH(S, x)
1 if S.top = S.length
2
error overflow
3 else S.top := S.top + 1
4
S[S.top] := x
POP(S)
1 if STACK-EMPTY(S)
2
error underflow
3 else S.top := S.top - 1
4
return S[S.top + 1]

Tutte le operazioni vengono eseguite in tempo T(n) = O(1)


poich la pila limitata, non servono cicli, quindi la complessit
costante

50

Code (queue) (1)


Le code sono simili alle pile, salvo che una
coda gestita con una politica FIFO (First In
First Out)
A livello astratto, una coda una collezione di
oggetti sulla quale si possono fare le seguenti
operazioni:
(controllare se vuota)
inserire un elemento nella collezione
(ENQUEUE)
cancellare un elemento dalla collezione
(DEQUEUE)
si noti che l'operazione di DEQUEUE
restituisce l'elemento cancellato
Una coda gestita con una politica FIFO
l'elemento che viene cancellato quello che
era stato inserito per primo (cio quello che
rimasto nella coda per pi tempo)

51

Code (queue) (2)


Se una coda pu contenere al pi n elementi,
allora, come per le pile, possiamo
implementarla tramite un array di lunghezza n
ora per dobbiamo tenere traccia di 2
indici:
l'indice del prossimo elemento da
eliminare (quello che nella coda da pi
tempo),
l'indice della cella nell'array in cui sar
memoerizzato il prossimo elemento
inserito nella coda
utilizziamo 2 attributi, head e tail
se Q una coda implementata mediante
un array, Q.head l'indice dell'elemento
da pi tempo nell'array
Q.tail l'indice in cui il prossimo
elemento inserito dovr essere
memorizzato
cio, Q.tail-1 l'indice dell'ultimo
elemento inserito

52

Operazioni sulle code


Prima di introdurre lo pseudocodice di
ENQUEUE e DEQUEUE, analizziamo come
funziona una coda implementata come array
gli elementi di una coda Q hanno indici
Q.head, Q.head+1, ... Q.tail-1
se Q.tail = Q.length e un nuovo elemento
inserito, il prossimo valore di tail sar 1
la coda funziona in modo circolare
per esempio, se la coda ha lunghezza 10,
Q.tail = 10 e noi inseriamo un nuovo
elemento, dopo l'accodamento abbiamo
che Q.tail = 1
se Q.head = Q.tail la coda vuota
se Q.head = Q.tail+1 la coda piena
se la coda non piena, c' sempre
almento una cella libera tra Q.tail e
Q.head
quindi, se dobbiamo implementare
mediante un array una coda Q che
contiene al massimo n elementi, l'array
deve avere n+1 celle

53

Pseudocodice per le operazioni sulle code

(che non controlla se la coda


piena/vuota)

tempo di esecuzione:
ENQUEUE(Q, x)
T(n) = O(1)
1 Q[Q.tail] := x
2 if Q.tail = Q.length
3
Q.tail := 1
4 else Q.tail := Q.tail + 1
tempo di esecuzione:
DEQUEUE(Q)
T(n) = O(1)
1 x := Q[Q.head]
2 if Q.head = Q.length
3
Q.head := 1
4 else Q.head := Q.head + 1
5
return x
e i controlli di coda piena/vuota?
(in realt c un po di ridondanza )
54

Liste (doppiamente) concatenate


Una lista concatenata una struttura dati in cui gli elementi
sono sistemati in un ordine lineare, in modo simile ad un array
l'ordine dato non dagli indici degli elementi, ma da una
catena di puntatori
Una lista doppiamente concatenata fatta di oggetti con 3
attributi:
key, che rappresenta il contenuto dell'oggetto
next, che il puntatore all'oggetto seguente
cio il successore dell'oggetto nell'ordinamento
lineare
prev, che il puntatore all'oggetto precedente
cio il predecessore
Se x un oggetto nella lista, se x.next = NIL, x non ha
successore
cio l'ultimo elemento della lista
Se x.prev = NIL, x non ha predecessore
cio il primo elemento della lista, la testa (head)
ogni lista L ha un attributo L.head, che il puntatore al primo
elemento della lista (ed eventualmente un L.tail)
Esempio di lista doppiamente concatenata
prev
head[L]

key
16

next
4

55

Altri tipi di liste:


singolarmente concatenate
gli elementi non hanno il puntatore prev
ordinate
l'ordinamento degli elementi nella lista
quello delle chiavi
il primo elemento ha la chiave minima,
l'ultimo la massima
non ordinate
circolari
il puntatore prev di head punta alla coda
(tail), e il puntatore next della coda punta alla
testa

56

Operazioni su una lista doppiamente


concatenata (1)
Ricerca
input: la lista L in cui cercare e la chiave k
desiderata
output: il puntatore ad un elemento che ha k
come chiave, NIL se la chiave non nella
lista
LIST-SEARCH(L, k)
1 x := L.head
2 while x NIL and
x.key k
3
x := x.next
4 return x
Nel caso pessimo (quando la chiave non
nella lista)
T(n) = (n)

57

Operazioni su una lista doppiamente


concatenata (2)
Inserimento (in testa)
input: la lista L, e l'oggetto x da aggiungere,
inizializzato con la chiave desiderata
output: inserisce x all'inizio della lista L
(anche se un elemento con la stessa
chiave esiste gi nella lista)
LIST-INSERT(L, x)
1 x.next := L.head
2 if L.head NIL
3
L.head.prev := x
4 L.head := x
5 x.prev := NIL
T(n) = O(1)

58

Operazioni (3)
Cancellazione
input: la lista L, e l'oggetto x da
cancellare
si noti che non si passa come
argomento la chiave da cancellare,
ma tutto l'oggetto
output: cancella x dalla lista
LIST-DELETE(L, x)
1 if x.prev NIL
2
x.prev.next := x.next
3 else L.head := x.next
4 if x.next NIL
5 x.next.prev := x.prev
//differenze tra C e Java?

T(n) = O(1)

59

Operazioni (4)

NB

se cancelliamo tramite la chiave, e non


direttamente tramite l'oggetto, allora la
complessit diventa O(n), perch dobbiamo
prima cercare l'elemento
dobbiamo cio prima chiamare LISTSEARCH, che ha tempo di esecuzione T(n)
= O(n)
Domanda_1: se L singolarmente concatenata
T(n) di LIST-DELETE(L, x) O(?)
Domanda_2: se voglio ordinare una lista
concatenata?

60

Dizionari e indirizzamento diretto

Dizionario: insieme dinamico che supporta solo le operazioni


di INSERT, DELETE, SEARCH
Agli oggetti di un dizionario si accede tramite le loro chiavi

Se la cardinalit m dell'insieme delle possibili chiavi U


(m=|U|) ragionevolmente piccola, la maniera pi semplice di
realizzare un dizionario tramite un array di m elementi
con questo si ha l'indirizzamento diretto
in questo caso l'array si dice tabella a indirizzamento diretto

Ogni elemento T[k] dell'array contiene il riferimento


all'oggetto di chiave k, se un tale oggetto stato inserito in
tabella, NIL altrimenti

0 NIL

U
18

4
2

1 NIL

7
...

T
2

dati

dati

3
4 NIL

61

Operazioni su una tabella a indirizzamento diretto


DIRECT-ADDRESS-SEARCH(T, k)
1 return T[k]
DIRECT-ADDRESS-INSERT(T, x)
1 T[x.key] := x
DIRECT-ADDRESS-DELETE(T, x)
1 T[x.key] := NIL

Hanno tutte T(n)=O(1)

Per, se il numero effettivamente memorizzato di chiavi


molto pi piccolo del numero di chiavi possibili, c' un sacco
di spreco di spazio... un po come il counting sort.

62

Tabelle hash
Una tabella hash usa una memoria proporzionale al
numero di chiavi effettivamente memorizzate nel
dizionario
indipendentemente dalla cardinalit dell'insieme
U di chiavi
Idea fondamentale: un oggetto di chiave k
memorizzato in tabella in una cella di indice h(k),
con h una funzione hash
se m la dimensione della tabella, h una
funzione
h: U {0..m-1}
la tabella T ha m celle, T[0], T[1], ... , T [m-1]
h(k) il valore hash della chiave k
Problema: ho |U| possibili chiavi ed una funzione
che le deve mappare su un numero m (< |U|, ma
tipicamente << |U|) di slot della tabella
necessariamente avr delle chiavi diverse
(tante!) k1, k2 tali che h(k1)=h(k2)
in questo caso ho delle collisioni
Ci sono diverse tecniche per risolvere le collisioni
Una tipica quella del concatenamento (chaining)
63

Risoluzione di collisioni tramite


concatenamento (1)

Idea della tecnica del concatenamento: gli


oggetti che vengono mappati sullo stresso slot
vengono messi in una lista concatenata

0 NIL
1

K
k2

T
k2

k3

2 NIL

k3

3
k5

k5

4 NIL
5 NIL

64

0 NIL

K
k2

k2

k3

2 NIL

k3

3
k5

k5

4 NIL
5 NIL

Operazioni sulle tabelle in questo caso:


CHAINED-HASH-INSERT(T, x)
inserisci x in testa alla lista T[h(x.key)]
CHAINED-HASH-SEARCH(T, k)
cerca un elemento con chiave k nella lista T[h(k)]
CHAINED-HASH-DELETE(T, x)
cancella x dalla lista T[h(x.key)]

INSERT si fa in tempo O(1) (assumendo l'elemento da


inserire non sia gi in tabella)
SEARCH si fa in tempo proporzionale alla lunghezza di
T[h(k)]
DELETE si fa in tempo O(1) se la lista doppiamente
concatenata
in input c' l'oggetto da eliminare, non solo la chiave
se singolarmente concatenata, proporzionale alla
lunghezza di T[h(x.key)]
65

Analisi della complessit delle


operazioni (1)
Nel caso pessimo, in cui tutti gli n
elementi memorizzati finiscono nello
stesso slot la complessit quella di una
ricerca in una lista di n elementi, cio O(n)
In media, per, le cose non vanno cos
male...
Siano:
m la dimensione della tabella (il numero
di slot disponibili)
il fattore di carico, = n/m
siccome 0 n |U| avremo 0
|U|/m
Ipotesi dell'hashing uniforme semplice:
ogni chiave ha la stessa probabilit 1/m di
finire in una qualsiasi delle m celle di T,
indipendentemente dalle chiavi
precedentemente inserite
66

Analisi della complessit delle


operazioni (2)
Sotto questa ipotesi, la lunghezza media di una
lista T[j]
m

E [ n j ]=

1
n
n
=
=

i
m i= 1
m

quindi il tempo medio per cercare una chiave k


non presente nella lista (1+)
1 il tempo per calcolare h(k), che si
suppone sia costante
(1+) anche il tempo medio per cercare
una chiave k che sia presente nella lista
la dimostrazione per richiede qualche
calcolo in pi...
In pratica:
se n = O(m), allora = n/m = O(m)/m =
O(1)
quindi in media ci mettiamo un tempo
costante
Quindi la complessit temporale O(1) (in
media) per tutte le operazioni (INSERT,
SEARCH, DELETE)
67

Funzioni hash (1)


Come scelgo una buona funzione hash h?
In teoria, ne dovrei prendere una che soddisfa l'ipotesi di
hashing uniforme semplice
per fare ci, per, dovrei sapere quale la
distribuzione di probabilit delle chiavi che devo
inserire
se le chiavi sono tutte vicine, la funzione hash
dovrebbe essere tale da riuscire a separarle
se invece so che le chiavi sono distribuite in
modo uniforme in
[0..K-1] mi basta prendere h(k) = (k/K)m
tipicamente si usano delle euristiche basate sul
dominio delle chiavi
Attenzione: tipica assunzione delle funzioni hash: la
chiave k un intero non-negativo (cio in )
facile convertire una qualunque informazione
trattata da un calcolatore in un intero non-negativo,
basta per esempio interpretare come tale la sequenza
di bit corrispondente
Quanto risulter uniformme?

68

Funzioni hash (2)


Come scelgo una buona funzione hash h?

Metodo della divisione:


h(k) = k mod m
facile da realizzare e veloce (una sola operazione)
evitare certi valori di m:
potenze di 2 (m non deve essere della forma 2p)
se no k mod m sono solo i p bit meno
significativi di k
meglio rendere h(k) dipendente da tutti i bit
di k
spesso si prende per m un numero primo non troppo
vicino ad una potenza esatta di 2
per esempio m=701, che ci darebbe, se n=2000,
in media 3 elementi per lista concatenata

69

Metodo della moltiplicazione (1)


Moltiplichiamo k per una costante A reale tale
che 0 < A < 1, quindi prendiamo la parte
frazionaria di kA; il risultato lo moltiplichiamo
per m, e ne prendiamo la parte intera
Cio:

h(k) = m(kA mod 1)


in cui x mod 1 = x x la parte frazionaria di x
In questo caso il valore di m non critico, funziona bene
con qualunque valore di A
spesso come m si prende una potenza di 2 (cio
m=2p), che rende semplice fare i conti con un
calcolatore
in questo caso, utile prendere come A un valore
che sia della forma s/2w, con w dimensione della
parola di memoria del calcolatore (con 0<s<2w)
se k sta in una sola parola (k<2w), ks=kA2w un
numero di 2w bit della forma r12w+r0, ed i suoi
w bit meno significativi (cio r0) costituiscono
kA mod 1
il valore di hash cercato (con m=2p) costituito
dai p bit pi significativi di r0

70

Metodo della moltiplicazione (2)


Un valore di A proposto (da Knuth) che
funziona bene

A ( 5 1) / 2
l'inverso della sezione aurea
se si vuole applicare il calcolo precedente,
occorre prendere come A la frazione della
forma s/2w pi vicina all'inverso della
sezione aurea
dipende dalla lunghezza della parola w

71

Indirizzamento aperto
Un altro modo di evitare collisioni tramite la tecnica
dell'indirizzamento aperto
In questo caso la tabella contiene tutte le chiavi, senza
memoria aggiuntiva
quindi il fattore di carico non potr mai essere pi
di 1
L'idea quella di calcolare l'indice dello slot in cui va
memorizzato l'oggetto; se lo slot gi occupato, si cerca
nella tabella uno slot libero
la ricerca dello slot libero per non viene fatta in
ordine 0,1,2,...,m-1; la sequenza di ricerca (detta
sequenza di ispezione) un valore calcolato dalla
funzione hash
dipende anche dalla chiave da inserire
la sequenza deve essere esaustiva, deve coprire
tutte le celle
La funzione hash ora diventa:
h : U {0,1,..., m-1} {0,1,..., m-1}
la sequenza di ispezione h(k, 0), h(k, 1),..., h(k, m1) deve essere una permutazione di 0, ... ,m-1

72

Operazioni in caso di indirizzamento aperto (1)


Inserimento di un oggetto:

HASH-INSERT(T, k)
1 i := 0
2 repeat
3
j := h(k, i)
4
if T[j] = NIL
5
T[j] := k
6
return j
7
else i := i + 1
8 until i = m
9 error hash table overflow

73

Operazioni in caso di indirizzamento aperto (2)


Ricerca:
HASH-SEARCH(T, k)
1 i := 0
2 repeat
3
j := h(k, i)
4
if T[j] = k
5
return j
6
else i := i + 1
7 until T[j] = NIL or i = m
8 return NIL
La cancellazione pi complicata, in quanto
non possiamo limitarci a mettere lo slot
desiderato a NIL, altrimenti non riusciremmo
pi a trovare le chiavi inserite dopo quella
cancellata
una soluzione quella di mettere nello slot,
invece che NIL, un valore convenzionale
come DELETED
per cos le complessit non dipendono
pi dal fattore di carico
74

Analisi di complessit (1)


Il tempo impiegato per trovare lo slot
desiderato (quello che contiene la chiave
desiderata, oppure quello libero in cui inserire
l'oggetto) dipende (anche) dalla sequenza di
ispezione restituita dalla funzione h
quindi dipende da come implementata h
Per semplificare un po' il calcolo facciamo
un'ipotesi sulla distribuzione di probabilit con
la quale vengono estratte non solo le chiavi,
ma anche le sequenze di ispezione
Ipotesi di hashing uniforme:
ognuna delle m! permutazioni di 0, ... ,m-1
ugualmente probabile che venga selezionata
come sequenza di ispezione
l'estensione dell'hashing uniforme
semplice visto prima al caso in cui
l'immagine sia non pi solo lo slot in cui
inserire l'elemento, ma l'intera sequenza di
ispezione

75

Analisi di complessit (2)


L'analisi viene fatta in funzione del fattore di
carico = n/m
siccome abbiamo al massimo un oggetto
per slot della tabella, nm, e 0 1
Sotto l'ipotesi di hashing uniforme valgono i
seguenti risultati (i calcoli sono sul libro):
il numero medio di ispezioni necessarie per
effettuare l'inserimento di un nuovo oggetto
nella tabella m se = 1 (se la tabella
piena), e non pi di 1/(1-) se <1 (se la
tabella cio ha ancora spazio disponibile)
il numero medio di ispezioni necessarie per
trovare un elemento presente in tabella
(m+1)/2 se =1, e non pi di
1/ log(1/(1-)) se <1

76

Tecniche di ispezione (1)


In pratica, costruire funzioni hash che
soddisfino l'ipotesi di hashing uniforme
molto difficile
Si accettano quindi delle approssimazioni che,
nella pratica, si rivelano soddisfacenti
Tre tecniche:
ispezione lineare
ispezione quadratica
doppio hashing
nessuna di queste tecniche produce le
m! permutazioni che sarebbero
necessarie per soddisfare l'ipotesi di
hashing uniforme
tuttavia, nella pratica si rivelano buone
a sufficienza
Tutte e 3 le tecniche fanno uso di una (o pi)
funzione hash ausiliaria (ordinaria) h': U
{0,1,..., m-1}

77

Tecniche di ispezione (2)


Ispezione lineare:
h(k,i) = (h'(k)+i) mod m
in questo caso l'ispezione inizia dalla cella
h'(k), e prosegue in h'(k)+1, h'(k)+2, ... fino
a che non si arriva a m-1, quindi si
ricomincia da 0 fino a esplorare tutti gli slot
di T
genera solo m sequenze di ispezione
distinte
la prima cella ispezionata identifica la
sequenza di ispezione
soffre del fenomeno dell'addensamento
(clustering) primario
lunghe sequenze di celle occupate
consecutive, che aumentano il tempo
medio di ricerca

78

Ispezione quadratica
Nel caso dell'ispezione quadratica:
h(k,i) = (h'(k)+c1i+c2i2) mod m
c1 e c2 sono costanti ausiliarie (con c2 0)
c1 e c2 non possono essere qualsiasi, ma
devono essere scelte in modo che la
sequenza percorra tutta la tabella
ancora una volta, la posizione di ispezione
iniziale determina tutta la sequenza, quindi
vengono prodotte m sequenze di ispezione
distinte
soffre del fenomeno dell'addensamento
secondario: chiavi con la stessa posizione
iniziale danno luogo alla stessa sequenza di
ispezione

79

Doppio hashing
Doppio hashing:
h(k,i) = (h1(k)+i h2(k)) mod m
h1 e h2 sono funzioni hash ausiliarie
perch la sequenza prodotta sia una
permutazione di 0, ... ,m-1 h2(k) deve
essere primo rispetto a m (non deve avere
divisori comuni tranne l'1)
posso ottenere questo prendendo come
m una potenza di 2, e facendo in modo
che h2 produca sempre un valore dispari
oppure prendendo come m un numero
primo, e costruendo h2 in modo che
restituisca sempre un valore < m
esempio:
h1(k) = k mod m
h2(k) = 1 + (k mod m')
con m' < m (per esempio m' = m-1)
numero di sequenze generate ora (m2) in
quanto ogni coppia (h1(k), h2(k)) produce
una sequenza di ispezione distinta

80

Alberi binari
Un albero binario fatto di 3 elementi: un
nodo radice; un albero binario che il
sottoalbero sinistro, ed un albero binario che
il sottoalbero destro
una definizione ricorsiva
un sottoalbero pu essere vuoto (NIL)
Ad ogni nodo dell'albero associamo un oggetto
con una chiave
Esempio di albero binario

radice
sottoalbero sinistro

3
2

7
1

sottoalbero destro

81

Rappresentazione di alberi binari (1)


Tipicamente si rappresentano alberi binari
mediante strutture dati concatenate
abbiamo per anche visto la rappresentazione
mediante array
Ogni nodo dell'albero rappresentato da un
oggetto che ha i seguenti attributi
key, la chiave del nodo (che per noi ne
rappresenta il contenuto)
tipicamente ci sono anche i dati satelliti
p, che il (puntatore al) nodo padre
left, che il (puntatore al) sottoalbero sinistro
left la radice del sottoalbero sinistro
right che il (puntatore al) sottoalbero destro
la radice del sottoalbero destro
ogni albero T ha un attributo, T.root, che il
puntatore alla radice dell'albero

82

Rappresentazione di alberi binari (2)


Si noti che:
se il sottoalbero sinistro (destro) di un nodo x
vuoto, allora
x.left = NIL
(x.right = NIL)
x.p = NIL se e solo se x la radice (cio x =
T.root)
Per esempio:
T.root
3

83

Alberi binari di ricerca (Binary Search Trees-1)


Un albero binario di ricerca (Binary Search
Tree, BST) un albero binario che soddisfa la
seguente propriet:
per tutti i nodi x del BST, se l un nodo nel
sottoalbero sinistro, allora l.key x.key; se r
un nodo del sottoalbero destro, allora
x.key r.key
tutti i nodi l del sottoalbero sinistro di un
nodo x sono tali che, per tutti i nodi r nel
sottoalbero destro di x vale l.key r.key
esempio
5

84

Alberi binari di ricerca (Binary Search Trees-2)


Una tipica operazione che viene fatta su un albero
di attraversarlo (walk through)
Lo scopo dell'attraversamento di un albero di
produrre (le chiavi associate a) gli elementi
dell'albero
Ci sono diversi modi di attraversare un albero; un
modo l'attraversamento simmetrico (inorder
tree walk)
prima si visita il sottoalbero sinistro e si
restituiscono i suoi nodi
quindi si restituisce la radice
quindi si visita il sottoalbero destro e si
restituiscono i suoi nodi
Si noti che:
come spesso accade con gli algoritmi sugli
alberi (che una struttura dati inerentemente
ricorsiva), l'attraversamento simmetrico un
algoritmo ricorsivo
con l'attraversamento simmetrico gli elementi
di un albero sono restituiti ordinati
per esempio, l'attraversamento simmetrico
sull'albero precedente produce le chiavi
seguenti: 2, 3, 5, 5, 7, 8
85

Algoritmi di attraversamento (1)


Nel dettaglio:
INORDER-TREE-WALK(x)
1 if x NIL
2
INORDER-TREE-WALK(x.left)
3
print x.key
4
INORDER-TREE-WALK(x.right)
Se T un BST, INORDER-TREEWALK(T.root) stampa tutti gli elementi di T
in ordine crescente
Se n il numero di nodi nel (sotto)albero, il
tempo di esecuzione per INORDER-TREEWALK (n)
se l'albero vuoto, eseguito in tempo
costante c
se l'albero ha 2 sottoalberi di dimensioni k e
n-k-1, T(n) dato dalla ricorrenza T(n) =
T(k) + T(n-k-1) + d, che ha soluzione
(c+d)n+c
lo si pu vedere sostituendo la soluzione
nell'equazione
86

Algoritmi di attraversamento (2)

Altre possibili strategie di attraversamento:


anticipato (preorder tree walk), e posticipato
(postorder tree walk)
in preorder, la radice restituita prima dei
sottoalberi
in postorder, la radice restituita dopo dei
sottoalberi

Esercizi:
scrivere lo pseudocodice per PREORDERTREE-WALK e POSTORDER-TREE-WALK
scrivere lo pseudocodice per Breadthfirst-TREE-WALK (il cui
risultato per lalbero
precedente deve essere:
5, 3, 7, 2, 5, 8)
87

Operazioni sui BST (1)


Sfruttiamo la propriet di essere un BST per
realizzare la ricerca:
confronta la chiave della radice con quella
cercata
se sono uguali, l'elemento quello cercato
se la chiave della radice pi grande, cerca
nel sottoalbero sinistro
se la chiave della radice pi grande, cerca
nel sottoalbero destro
TREE-SEARCH(x, k)
1 if x = NIL or k = x.key
2
return x
3 if k < x.key
4
return TREE-SEARCH(x.left, k)
5 else return TREE-SEARCH(x.right, k)

Il tempo di esecuzione O(h), con h l'altezza


dell'albero

88

Operazioni sui BST (2)

L'elemento minimo (risp. massimo) in un BST


quello che pi a sinistra (risp. destra)
Sfruttiamo questa propriet per definire il
seguente algoritmo, che semplicemente
scende nell'albero
MINIMUM scende a sinistra, mentre
MAXIMUM scende a destra
gli algoritmi restituiscono l'oggetto
nell'albero con la chiave minima, non la
chiave stessa
Entrambi gli algoritmi hanno tempo di
esecuzione che O(h), con h l'altezza
dell'albero
TREE-MINIMUM(x)
1 while x.left NIL
2
x := x.left
3 return x

TREE-MAXIMUM(x)
1 while x.right NIL
2
x := x.right
3 return x

89

Operazioni sui BST (3)


Il successore (risp. predecessore) di un oggetto x in un
BST l'elemento y del BST tale che y.key la pi
piccola (risp. pi grande) tra le chiavi che sono pi
grandi (risp. piccole) di x.key
di fatto, se il sottoalbero destro di un oggetto x
dell'albero non vuoto, il successore di x
l'elemento pi piccolo (cio il minimo) del
sottoalbero destro di x
invece, se il sottoalbero destro di x vuoto, il
successore di x il primo elemento y che si incontra
risalendo nell'albero da x tale che x nel sottoalbero
sinistro di y
TREE-SUCCESSOR(x)
1 if x.right NIL
2
return TREE-MINIMUM(x.right)
3 y := x.p
4 while y NIL and x = y.right
5
x := y
6
y := y.p
7 return y

Il successore del massimo NIL


Il tempo di esecuzione per TREE-SUCCESSOR O(h)
Esercizio: scrivere l'algoritmo TREE-PREDECESSOR e
darne la complessit
90

Inserimento (1)
Idea di base per l'inserimento: scendere
nell'albero fino a che non si raggiunge il posto
in cui il nuovo elemento deve essere inserito,
ed aggiungere questo come foglia
Supponiamo, per esempio, di volere inserire un
nodo con chiave 7 nell'albero seguente:

91

Inserimento (2)
eseguiamo i seguenti passi:
confrontiamo 5 con 7 e decidiamo che il
nuovo elemento deve essere aggiunto al
sottoalbero destro di 5
confrontiamo 8 con 7 e decidiamo che 7
deve essere aggiunto al sottoalbero sinistro
di 8
notiamo che il sottoalbero sinistro di 8
vuoto, e aggiungiamo 7 come sottoalbero
sinistro di 8
quindi, otteniamo il nuovo albero:
5

92

Insert: pseudocodice
TREE-INSERT(T, z)
1
y := NIL
2
x := T.root
3
while x NIL
4
y := x
5
if z.key < x.key
6
x := x.left
7
else x := x.right
8
z.p := y
9
if y = NIL
10
T.root := z
//l'albero T era vuoto
11 elsif z.key < y.key
12
y.left := z
13 else y.right := z

Si noti che inseriamo un oggetto, z, che


assumiamo sia stato inizializzato con la chiave
desiderata

Il tempo di esecuzione di TREE-INSERT


O(h)
infatti, scendiamo nell'albero nel ciclo
while (che al massimo richiede tante
ripetizioni quanta l'altezza dell'albero), e
il resto (linee 8-13) si fa in tempo costante

93

Cancellazione (1)
Quando cancelliamo un oggetto z da un albero, abbiamo
3 possibili casi (a seconda che z sia una foglia o un nodo
interno):
il nodo z da cancellare non ha sottoalberi
il nodo z da cancellare ha 1 sottoalbero
il nodo z da cancellare ha 2 sottoalberi
Il caso 1 quello pi facile, basta mettere a NIL il
puntatore del padre di z che puntava a z:

5
3
1

5
8

3
9

Nel caso 2,dobbiamo spostare l'intero sottoalbero di z su di un


livello:
5
3
1

5
7

3
9

9
4

10

10

94

Cancellazione(2)
Nel caso 3 dobbiamo trovare il successore del nodo da
cancellare z, copiare la chiave del successore in z, quindi
cancellare il successore
cancellare il successore potrebbe richiedere di
spostare un (il) sottoalbero del successore un livello
su
si noti che in questo caso l'oggetto originario z non
cancellato, ma il suo attributo key viene modificato
(l'oggetto effettivamente cancellato quello con il
successore di z)
5
3

12
8

14
9

5
3
1

8
4

12
9

14

95

Delete: pseudocodice (1)


TREE-DELETE(T, z)
1
if z.left = NIL or z.right = NIL
2
y := z
3
else y := TREE-SUCCESSOR(z)
4
if y.left NIL
5
x := y.left
6
else x := y.right
7
if x NIL
8
x.p := y.p
9
if y.p = NIL
10
T.root := x
11 elsif y = y.p.left
12
y.p.left := x
13 else y.p.right := x
14 if y z
15
z.key := y.key
16 return y

96

Delete: pseudocodice (2)


In TREE-DELETE, y il nodo effettivamente da
cancellare
Se z ha non pi di un sottoalbero, allora il nodo y
da cancellare z stesso; altrimenti (se z ha
entrambi i sottoalberi) il suo successore (linee
1-3)
Si noti che y non pu avere pi di un
sottoalbero
nelle linee 4-6, ad x viene assegnata la radice del
sottoalbero di y se y ne ha uno, NIL se y non ha
sottoalberi
le linee 7-13 sostituiscono y con il suo
sottoalbero (che ha x come radice)
nelle linee 14-15, se z ha 2 sottoalberi (che
corrisponde caso in cui il nodo y da cancellare
il successore di z, non z stesso), la chiave di z
sostituita con quella del suo successore y

97

Analisi di complessit (1)


Il tempo di esecuzione per TREE-DELETE
O(h)
TREE-SUCCESSOR O(h), il resto fatto
in tempo costante
Tutte le operazioni sui BST (SEARCH,
MINIMUM, MAXIMUM, SUCCESSOR,
PREDECESSOR, INSERT, DELETE) hanno
tempo di esecuzione che O(h)
cio, alla peggio richiedo di scendere
nell'albero
Quindi... quanto vale l'altezza di un BST
(rispetto al numero dei suoi nodi)?

98

Analisi di complessit (2)


Per un albero completo, h = (log(n))
un albero completo se e solo se, per ogni
nodo x, o x ha 2 figli, o x una foglia, e
tutte le foglie hanno la stessa profondit
5

Nel caso pessimo, per, che si ha se tutti i nodi


sono in linea, abbiamo h = (n)

2
4
5
9

99

Analisi di complessit (3)


Tuttavia, un BST non deve per forza essere
completo per avere altezza h tale che h =
(log(n))
abbiamo per esempio visto che questa propriet vale
anche per alberi quasi completi

Abbiamo che h = (log(n)) anche per un


albero bilanciato
informalmente, diciamo che un albero bilanciato
se e solo se non ci sono 2 foglie nell'albero tali che
una molto pi lontana dalla radice dell'altra (se
si trovano a profondit molto diverse)
ci potrebbero essere diverse nozioni di "molto
pi lontano"
una possibile definizione di albero bilanciato
(Adelson-Velskii e Landis) la seguente: un
albero bilanciato se e solo se, per ogni nodo x
dell'albero, le altezze dei 2 sottoalberi di x
differiscono al massimo di 1
per esempio
5

100

Analisi di complessit (4)

Ci sono diverse tecniche per mantenere un


albero bilanciato:
alberi rosso-neri (red-black)
alberi AVL
B- trees
etc.
Inoltre, si pu dimostrare che l'altezza attesa di
un albero O(log(n)) se le chiavi sono inserite
in modo casuale

101

Alberi rosso-neri (red-black) (1)


Gli alberi rosso-neri (RB) sono BST
abbastanza bilanciati, tali che l'altezza
dell'albero h O(log(n))
ed possibile realizzare tutte le operazioni pi
importanti in tempo O(log(n))

Negli alberi RB non si ha mai che un ramo


dell'albero sia lungo pi del doppio di un altro
ramo
una nozione di bilanciamento diversa da quella
degli alberi AVL, ma d comunque h = O(log(n))

Idea alla base degli alberi RB:


ogni nodo ha un colore, che pu essere solo rosso o
nero
i colori sono distribuiti nell'albero in modo da
garantire che nessun ramo dell'albero sia 2 volte pi
lungo di un altro

102

Alberi rosso-neri (red-black) (2)


Ogni nodo di un albero RB ha 5 attributi: key,
left, right, p, e color
convenzione: le foglie sono i nodi NIL, tutti i nodi
non NIL (che hanno quindi una chiave associata)
sono nodi interni

Un BST un albero RB se soddisfa le seguenti


5 propriet:
1.
2.
3.
4.
5.

ogni nodo o rosso o nero


la radice nera
le foglie (NIL) sono tutte nere.
i figli di un nodo rosso sono entrambi neri
per ogni nodo x tutti i cammini da x alle foglie sue
discendenti contengono lo stesso numero bh(x) di
nodi neri
bh(x) la altezza nera (black height) del nodo x
Il nodo x non contato in bh(x) anche se nero.

103

Esempi di alberi RB
26
17

41

14

21

10
7

16
12

19

15

30
23

47

28

20

38
35

39

Per comodit, per rappresentare tutte le foglie NIL, si usa un


unico nodo sentinella T.nil
un nodo particolare accessibile come attributo dell'albero T
tutti i riferimenti a NIL (compreso il padre della radice) sono
sostituiti con riferimenti a T.nil
26

17

41

14

21

10

16
12

15

19

30
23

47

28

20

38
35

39

T.nil

104

Propriet degli alberi RB

Un albero rosso-nero con n nodi interni (n nodi con chiavi, per


la convenzione usata) ha altezza
h 2 log2(n+1)
si dimostra che il numero di nodi interni di un (sotto)albero con
radice x 2bh(x)-1
per la propriet 4, almeno met dei nodi dalla radice x (esclusa)
ad una foglia sono neri, quindi bh(x) h/2, e n 2h/2-1, da cui
discende che h 2 log2(n+1)

Come conseguenza di questa propriet, SEARCH, MINIMUM,


MAXIMUM, SUCCESSOR e PREDECESSOR richiedono tempo
O(log(n)) se applicate ad un albero RB con n nodi
queste operazioni non modificano l'albero, che viene
semplicemente trattato come un BST
il loro pseudocodice come quello visto in precedenza

INSERT e DELETE si possono anch'esse fare con complessit


O(log(n)), ma devono essere modificate rispetto a quelle viste
prima
devono essere tali da mantenere le 5 propriet degli alberi RB

Il meccanismo fondamentale per realizzare INSERT e


DELETE quello delle rotazioni

105

Rotazioni

Le rotazioni possono essere verso sinistra (LEFT-ROTATE) o


verso destra (RIGHT-ROTATE)

LEFT-ROTATE(T,x)

RIGHT-ROTATE(T,y)

LEFT-ROTATE(T,x)
1 y := x.right
2 x.right := y.left
3
4
5
6
7
8
9
10
11
12

//il sottoalbero sinistro di y


//diventa quello destro di x

if y.left T.nil
y.left.p := x
y.p := x.p
//attacca il padre di x a y
if x.p = T.nil
T.root := y
elsif x = x.p.left
x.p.left := y
else x.p.right := y
y.left := x
//mette x a sinistra di y
x.p := y

Una rotazione su un BST mantiene la propriet di essere BST


Esercizio per casa: scrivere lo pseudocodice per RIGHTROTATE

106

RB-INSERT
L'inserimento fatto in modo analogo a quello dei BST, ma
alla fine occorre ristabilire le propriet dagli alberi RB se
queste sono state violate
per ristabilire le propriet si usa un algoritmo RB-INSERTFIXUP (che vedremo dopo)

RB-INSERT(T, z)
1
y := T.nil
2
x := T.root
3
while x T.nil
4
y := x
5
if z.key < x.key
6
x := x.left
7
else x := x.right
8
z.p := y
9
if y = T.nil
10
T.root := z
//l'albero T e' vuoto
11 elsif z.key < y.key
12
y.left := z
13 else y.right := z
14 z.left := T.nil
15 z.right := T.nil
16 z.color := RED
17 RB-INSERT-FIXUP(T, z)
Uguale a TREE-INSERT, salvo che per l'uso di T.nil al
posto di NIL e l'aggiunta delle righe 14-17
107

RB-INSERT-FIXUP
RB-INSERT-FIXUP(T, z)
1 if z = T.root
2
T.root.color = BLACK
3 else x := z.p
// x e' il padre di z
4
if x.color = RED
5
if x = x.p.left
// se x e' figlio sin.
6
y := x.p.right
// y e' lo zio di z
7
if y.color = RED
8
x.color := BLACK
// Caso 1
9
y.color := BLACK
// Caso 1
10
x.p.color := RED
// Caso 1
11
RB-INSERT-FIXUP(T,x.p)
// Caso 1
12
else if z = x.right
13
z := x
// Caso 2
14
LEFT-ROTATE(T, z)
// Caso 2
15
x := z.p
// Caso 2
16
x.color := BLACK
// Caso 3
17
x.p.color := RED
// Caso 3
18
RIGHT-ROTATE(T, x.p)
// Caso 3
19
else (come 6-18, scambiando rightleft)

RB-INSERT-FIXUP invocato sempre su un nodo z


tale che z.color = RED
per questo motivo, se la condizione alla linea 4
non verificata (cio se il padre di z di colore nero)
non ci sono ulteriori modifiche da fare
108

Funzionamento di RB-INSERT-FIXUP

Caso 1: y rosso
quindi x.p, che anche y.p, non pu essere rosso o l'albero
originario avrebbe violato la propriet 4

x.p

x.p

x 5

x 5

7
y

z 3

z 3

x.p
x 3
z

x.p

7
9
5

x 3

7
9

z 5

quindi ripeto la procedura su x.p, in quanto il padre di x.p


potrebbe essere di colore rosso, nel qual caso la propriet 4 degli
alberi RB non sarebbe (ancora) verificata

109

Funzionamento di RB-INSERT-FIXUP (2)

Caso 2: y nero e z figlio destro di x


x.p
x 3

x' = z

9 y

z 5 d

x.p

z' = x 3

9 y

5
d

a questo punto ci siamo messi nel caso 3

Caso 3: y nero e z figlio sinistro di x


x.p
x 5

9 y

z 3

x.p

x 5

z 3

5
9 y

9
d

a questo punto l'albero a posto, non ci sono pi modifiche da


fare

Ogni volta che RB-INSERT-FIXUP viene invocato esso pu


o terminare (casi 2 e 3), o venire applicato ricorsivamente
risalendo 2 livelli nell'albero (caso 1)
quindi pu essere invocato al massimo O(h) volte, cio O(log(n))
si noti che una catena di invocazioni di RB-INSERT-FIXUP
esegue al massimo 2 rotazioni (l'ultima chiamata)
110

RB-DELETE
RB-DELETE(T, z)
1
if z.left = T.nil or z.right = T.nil
2
y := z
3
else y := TREE-SUCCESSOR(z)
4
if y.left T.nil
5
x := y.left
6
else x := y.right
7
x.p := y.p
8
if y.p = T.nil
9
T.root := x
10 elsif y = y.p.left
11
y.p.left := x
12 else y.p.right := x
13 if y z
14
z.key := y.key
15 if y.color = BLACK
16
RB-DELETE-FIXUP(T,x)
17 return y

Uguale a TREE-DELETE, salvo che per l'uso di T.nil al posto


di NIL (che permette l'eliminazione dell'if alla linea 7), e
l'aggiunta delle righe 15-16
Se viene cancellato un nodo rosso (cio se y.color = RED)
non c' bisogno di modificare i colori dei nodi
per come fatto RB-DELETE, viene cancellato un nodo (y) che
ha al massimo un figlio diverso da T.nil, e se y.color = RED
il nodo x che prende il posto di y per forza nero
111

RB-DELETE-FIXUP
RB-DELETE-FIXUP(T, x)
1

if x.color = RED or x.p = T.nil

x.color := BLACK

elsif x = x.p.left

w := x.p.right

if w.color = RED

// Caso 0
// x e' figlio sinistro
// w e' fratello di x

w.color := BLACK

// Caso 1

x.p.color := RED

// Caso 1

LEFT-ROTATE(T,x.p)

// Caso 1

w := x.p.right

// Caso 1

10

if w.left.color = BLACK and


w.right.color = BLACK

11

w.color := RED

// Caso 2

12

RB-DELETE-FIXUP(T,x.p)

// Caso 2

13

else if w.right.color = BLACK

14

w.left.color := BLACK

// Caso 3

15

w.color := RED

// Caso 3

16

ROTATE-RIGHT(T,w)

// Caso 3

17

w := x.p.right

// Caso 3

18

w.color := x.p.color

// Caso 4

19

x.p.color := BLACK

// Caso 4

20

w.right.color := BLACK

// Caso 4

21

ROTATE-LEFT(T,x.p)

// Caso 4

19 else (come 4-21, scambiando rightleft)

Idea: il nodo x passato come argomento si porta dietro un


nero in pi, che, per fare quadrare i conti, pu essere
eliminato solo a certe condizioni

112

Funzionamento di RB-DELETE-FIXUP

x ha preso il posto del nodo eliminato

Caso 0: x un nodo rosso, oppure la radice;


x 5

x 5

x 5

x 5

Caso 1: x un nodo nero, il suo fratello destro w rosso, e di


conseguenza il padre x.p nero
3

x 1

7 w

x 1

9
d

7 w

5 w

x 1

diventa o il caso 2, o il caso 3, o il caso 4

Caso 2: x nero, suo fratello destro w nero con figli entrambi neri
3
x

3
7 w

9
d

7 w

9
d

se arriviamo al caso 2 dal caso 1, allora x.p rosso, e quando RBDELETE-FIXUP viene invocato su di esso termina subito (arriva subito al
caso 0)
113

Funzionamento di RB-DELETE-FIXUP (2)

Caso 3: x nero, suo fratello destro w nero con figlio sinistro


rosso e figlio destro nero
3

x 1

3
7 w

x 1

7 w

x 1

5 w

diventa il caso 4

Caso 4: x nero, suo fratello destro w nero con figlio destro


rosso
3
3
7
7 w

9
d

7 w

9
d

Ogni volta che RB-DELETE-FIXUP viene invocato esso pu


o terminare (casi 0, 1, 3 e 4), o venire applicato ricorsivamente
risalendo un livello nell'albero (caso 2 non proveniente da 1)
quindi pu essere invocato al massimo O(h) volte, cio O(log(n))
si noti che una catena di invocazioni di RB-DELETE-FIXUP
esegue al massimo 3 rotazioni (se il caso 1 diventa 3 e poi 4)
114

Richiamo sui grafi

Un grafo una coppia G = (V, E), in cui:


V un insieme di nodi (detti anche vertici)
E un insieme di archi (detti anche lati, o edges)

Un arco una connessione tra 2 vertici


2 vertici connessi da un arco sono detti adiacenti
se un arco e connette 2 vertici u e v, pu essere rappresentato
dalla coppia (u, v) di vertici che connette
quindi, E V2

|V| il numero di vertici nel grafo, mentre |E| il numero di


archi
0 |E| |V|2

Ci sono 2 tipi di grafi: orientati e non orientati


in un grafo non orientato, un arco (u, v) lo stesso di (v, u) (non
c' nozione di direzione da un nodo all'altro)
In un grafo orientato(u, v) "va dal" nodo u al nodo v, ed diverso
da (v, u)

Esempio di grafo non orientato


a

d
V = {a, b, c, d, e}
E = {(b, a) (a, c) (b, c) (d, c) (e, d) (b, e)}

e
b
L'ordine dei vertici negli archi irrilevante

Esempio di grafo orientato:

c
b

V = {a, b, c, d, e}
E = {(a, b) (a, d) (d, a) (b, e)
(c, e) (e, d), (e, e)}

115

Rappresentazione di grafi in memoria (1)


Come possiamo rappresentare un grafo in
memoria?
2 tecniche principali:
liste di adiacenza
matrice di adiacenza
Grafo orientato
a

c
e
b

V = {a, b, c, d, e}
E = {(a, b) (a, d) (d, a) (b, e)
(c, e) (e, d), (e, e)}

a
b
c
d
e

Liste ad.
d

b c

[ ]
0
0
0
1
0

1
0
0
0
0

0
0
0
0
0

1
0
0
0
1

0
1
1
0
1

Matrice ad.

116

Rappresentazione di grafi in memoria (2)

d
c

a
b
c
d
e

b
e
e
a
e

V = {a, b, c, d, e}
E = {(a, b) (a, d) (d, a) (b, e)
(c, e) (e, d), (e, e)}

a
b
c
d
e

b c

d
Liste ad.
d

[ ]
0
0
0
1
0

1
0
0
0
0

0
0
0
0
0

1
0
0
0
1

0
1
1
0
1

Matrice ad.

Nel caso di liste di adiacenza abbiamo un array


di liste
c' una lista per ogni nodo del grafo
per ogni vertice v, la lista corrispondente
contiene i vertici adiacenti a v
In una matrice di adiacenza M, l'elemento mij
1 se c' un arco dal nodo i al nodo j, 0
altrimenti
In entrambi i casi, dato un nodo u in un grafo
G, l'attributo u.Adj rappresenta l'insieme di
vertici adiacenti a u

117

Rappresentazione di grafi (3)


Quanto grande una rappresentazione con liste
di adiacenza?
il numero totale di elementi nelle liste |E|
il numero di elementi nell'array |V|
la complessit spaziale (|V| + |E|)
Quanto grande la matrice di adiacenza?
la dimensione della matrice |V|2, quindi la
sua complessit (|V|2)
Le liste di adiacenza sono in generale migliori
quando |E| (|V|2), cio quando il grafo
sparso (quando il numero di nodi connessi
non tanto grande)
si ricordi che |E| |V|2, cio |E| = O(|V|2)
Se il grafo completo (o quasi), tanto vale
usare una matrice di adiacenza
un grafo orientato completo se, per ogni
coppia di nodi u e v, sia l'arco (u, v) che
l'arco (v, u) sono in E

118

Rappresentazione di grafi (4)


Quale la complessit temporale per
determinare se un arco (u, v) appartiene al
grafo:
quando il grafo rappresentato mediante
liste di adiacenza?
quando il grafo rappresentato con una
matrice di adiacenza?
Quale la complessit temporale per
determinare il numero di archi che escono da
un nodo u del grafo
quando il grafo rappresentato mediante
liste di adiacenza?
quando il grafo rappresentato con una
matrice di adiacenza?

119

Rappresentazione di grafi (5)


Possiamo rappresentare un arco (u, v) di un
grafo non orientato come 2 archi orientati, uno
che va da u a v, ed uno che va da v ad u
Per esempio:
a

c
e

a
a
b

V = {a, b, c, d, e}
E = {(b, a) (a, c) (b, c) (d, c) (e, d) (b, e)}c

d
e

[ ]
0
1
1
0
0

1
0
1
0
1

1
1
0
1
0

0
0
1
0
1

0
1
0
1
0

si noti che in questo caso la matrice di adiacenza


simmetrica, quindi tutta l'informazione che serve per
descrivere il grafo si trova sopra la diagonale
principale

120

Visita in ampiezza (Breadth-First Search) (1)


Problema:
input: un grafo G, e un nodo s (la sorgente)
di G
output: visitare tutti i nodi di G che sono
raggiungibili da s (un nodo u
raggiungibile da s se c' un cammino nel
grafo che va da s a u)
Algoritmo: Breadth-First Search (BFS)
Idea dell'algoritmo: prima visitiamo tutti i nodi
che sono a distanza 1 da s (cio che hanno un
cammino di lunghezza 1 da s), quindi visitiamo
quelli a distanza 2, quindi quelli a distanza 3, e
cos via
Quando visitiamo un nodo u, teniamo traccia
della sua distanza da s in un attributo u.dist

121

Visita in ampiezza (Breadth-First Search) (2)


Inoltre, mentre visitiamo i nodi, li coloriamo
(cio li marchiamo per tenere traccia della
progressione dell'algoritmo)
un nodo bianco se deve essere ancora visitato
un nodo grigio se lo abbiamo gi visitato, ma
dobbiamo ancora completare la visita dei nodi ad
esso adiacenti
un nodo nero dopo che abbiamo visitato tutti i suoi
nodi adiacenti

L'algortitmo in breve:
all'inizio tutti i nodi sono bianchi, tranne s (la
sorgente), che grigio
manteniamo i nodi di cui dobbiamo ancora visitare i
nodi adiacenti in una coda (che gestita con politica
FIFO!)
all'inizio la coda contiene solo s
a ogni iterazione del ciclo, eliminiamo dalla coda un
elemento u, e ne visitiamo i nodi adiacenti che sono
ancora bianchi (cio che devono essere ancora
visitati)
Si noti che, se u.dist la distanza del nodo u da
s, la distanza dei nodi bianchi adiacenti ad u
u.dist+1 (a meno che non mettiamo dei pesi agli
archi )

122

BFS: pseudocodice
BFS(G, s)
1 for each u G.V {s}
2
u.color := WHITE
3
u.dist :=
4
s.color := GRAY
5
s.dist := 0
6
Q :=
7
ENQUEUE(Q, s)
8
while Q
9
u := DEQUEUE(Q)
10
for each v u.Adj
11
if v.color = WHITE
12
v.color := GRAY
13
v.dist := u.dist +1
14
ENQUEUE(Q, v)
15
u.color := BLACK

Le linee 1-7 sono la fase di inizializzazione dell'algoritmo (che


ha complessit O(|V|))
Le linee 8-15 sono quelle che effettivamente visitano i nodi;
ogni nodo nel grafo G accodato (e tolto dalla coda) al
massimo una volta, quindi, nel ciclo for della linea 10, ogni
lato visitato al massimo una volta; quindi, la complessit del
ciclo while O(|E|)
La complessit totale di BFS O(|V| + |E|)

123

Ricerca in profondit (Depth-First Search) (1)


BFS si basa sull'idea di visitare i nodi con una
politica FIFO (che realizzata mediante una
coda)
Come alternativa possiamo usare una politica
LIFO
Se usiamo una politica LIFO, otteniamo un
algoritmo di visita in profondit (depth-first
search, DFS)
l'idea in questo caso che, ogni volta che mettiamo
un nodo in cima allo stack, immediatamente
cominciamo a visitare i nodi a lui adiacenti
cio continuiamo con la visita dei nodi che sono
adiacenti a quello che da meno tempo nello
stack
in BFS non cos: visitiamo i nodi che sono
adiacenti a quello che da pi tempo nella coda
non sufficiente, per ottenere un algoritmo di DFS,
cambiare ENQUEUE con PUSH, e DEQUEUE con
POP nell'algoritmo di BFS...
non appena visitiamo un nodo, dobbiamo
ripetere DFS su di esso...
DFS implementato in modo naturale in modo
ricorsivo

124

Ricerca in profondit (Depth-First Search) (2)


In realt, l'algoritmo DFS risolve un problema
leggermente diverso da BFS
Problema risolto dall'algoritmo DFS
input: un grafo G
output: visitare tutti i nodi di G
nell'algoritmo BFS visitiamo solo i nodi
che sono raggiungibili dalla sorgente s!
DFS spesso usato come parte (cio come
sottoalgoritmo) di un algoritmo pi complesso
(da cui il problema leggermente diverso da
quello risolto da BFS)
spesso usato come passo preparatorio
prima di lanciare l'algoritmo principale

125

DFS: considerazioni
Come in BFS, in DFS coloriamo i nodi di bianco, grigio
e nero (con lo stesso significato che in BFS)
Come detto in precedenza, questa volta usiamo una
politica di visita LIFO, quindi usiamo un meccanismo
analogo a quello dello stack
in questo caso, il meccanismo a stack viene dal fatto
che l'algoritmo ricorsivo
invece di fare push e pop di vertici su uno stack,
facciamo push e pop di chiamate ricorsive
dell'algoritmo sullo stack delle chiamate (vedere
corsi base informatica):
push = invochiamo l'algoritmo
ricorsivamente
pop = la chiamata ricorsiva termina
L'algoritmo DFS tiene traccia di quando i nodi sono
messi sullo stack ed anche di quando sono tolti da
esso
c' una variabile (globale), time, che messa a 0
nella fase di inizializzazione dell'algoritmo, e che
incrementata di 1 sia appena dopo un nodo messo
sullo stack che appena prima di togliere un nodo
dallo stack
usiamo la variabile time per tenere traccia di 2 altri
valori:
il tempo di quando inizia la scoperta
(discovery) di un nodo, ed il tempo di quando
la scoperta termina
l'inizio della scoperta di un nodo u
memorizzata nell'attributo u.d, mentre la sua fine
nell'attributo u.f
126

DFS: pseudocodice
DFS(G)
1 for each u G.V
2
u.color := WHITE
3 time := 0
4 for each u G.V
5
if u.color = WHITE
6
DFS-VISIT(u)

in cui l'algoritmo DFS-VISIT il seguente:


DFS-VISIT(u)
1 u.color := GRAY
2 time := time + 1
3 u.d := time
4 for each v u.Adj
5
if v.color = WHITE
6
DFS-VISIT(v)
7 u.color := BLACK
8 u.f := time := time + 1

Le linee 1-3 di DFS inizializzano i nodi colorandoli tutti di


bianco, e mette il tempo a 0
tempo di esecuzione (|V|)
L'algoritmo DFS-VISIT ripetuto (linee 4-6 di DFS) fino a che
non ci sono pi nodi da visitare
come in BFS, ogni nodo messo sullo stack (che in questo
caso corrisponde ad invocare DFS-VISIT sul nodo) solo
una volta
quindi, ogni lato visitato esattamente una volta durante
l'esecuzione del ciclo for delle linee 4-6 di DFS, quindi
127
queste prendono tempo (|E|)
In tutto, la complessit di DFS (|V| + |E|)

Ordinamento Topologico (1)


Supponiamo di avere un grafo orientato
aciclico (directed acyclic graph, DAG) che
rappresenta le precedenze tra eventi
Come questo, per esempio

mutande

calze
orologio

pantaloni

scarpe

camicia
cintura
cravatta

giacca

128

Ordinamento Topologico (2)

O, se preferite, come questo (che rappresenta


un Network Part Program di un Flexible
Manufacturing System)

begin

4
3

10

6
5
7

end

129

Ordinamento topologico (3)


Un ordinamento topologico di un DAG un
ordinamento lineare dei nodi del grafo tale che, se nel
DAG c' un arco (u, v), allora il nodo u precede v
nell'ordinamento
Per esempio, un ordinamento topologico del primo
DAG della slide precedente potrebbe dare il seguente
ordinamento

calze

mutande

pantaloni

scarpe

orologio camicia

cintura cravatta

giacca

si noti che questo non l'unico possibile


ordinamento ammissibile...
Di fatto, un ordinamento topologico restituisce un
ordinamento che rispetta le precedenze tra eventi
per esempio, nel caso del network part program, un
ordinamento topologico restituisce una sequenza di
operazioni che compatibile con le precedenze tra
di esse
cio tale che, quando eseguiamo Oi, tutte le
operazioni preparatorie necessarie sono state
completate

130

Ordinamento topologico (4)

Il problema dell'ordinamento topologico di un


DAG il seguente:
input: un DAG G
output: una lista che un ordinamento
topologico di G
si ricordi che una lista un ordinamento
lineare, in cui l'ordine dato da come gli
oggetti nella lista sono connessi tra loro
Idea per l'algoritmo:
visitiamo il DAG con un algoritmo DFS
quando coloriamo un nodo u di G di nero
(cio ogni volta che finiamo di visitare un
nodo di G), inseriamo u in testa alla lista
dopo che abbiamo visitato tutti i nodi di G,
la lista che abbiamo costruito un
ordinamento topologico di G, e lo
restituiamo

131

Ordinamento topologico: pseudocodice


TOPOLOGICAL-SORT(G)
1 L :=
2 for each u G.V
3
u.color := WHITE
4 for each u G.V
5
if u.color = WHITE
6
TOPSORT-VISIT(L, u)
7 return L

in cui TOPSORT-VISIT :
TOPSORT-VISIT(L, u)
1 u.color := GRAY
2 for each v u.Adj
3
if v.color = WHITE
4
TOPSORT-VISIT(L, v)
5 crea l'elemento di lista x
6 x.key := u
7 LIST-INSERT(L, x)
8 u.color := BLACK

Il tempo di esecuzione di TOPSORT lo stesso di DFS, cio


(|V| + |E|)
le linee 5-7 di TOPSORT-VISIT impiegano tempo (1)
(come le linee 2-3 e 7-8 di DFS-VISIT), ed il resto
dell'algoritmo come DFS
tranne la gestione della variabile time, che possiamo
evitare
132

Argomenti Avanzati

133

Programmazione dinamica (1)


(cenni)
Come la tecnica divide-et-impera si basa
sull'idea di scomporre il problema in
sottoproblemi, risolvere quelli, e ricombinarli
si applica per quando i problemi non sono
indipendenti, cio condividono dei
sottoproblemi
quando si risolve un sottoproblema
comune, si mette la soluzione in una
tabella, per riutilizzarla in seguito
il termine programmazione qui non si
riferisce alla codifica in linguaggi di
programmazione, ma al fatto che una
tecnica tabulare
Programmazione dinamica spesso usata per
problemi di ottimizzazione
la soluzione un ottimo del
sottoproblema
un problema potrebbe avere pi
soluzioni ottime

134

Programmazione dinamica (2)


Tipici passi nello sviluppo di un algoritmo di
programmazione dinamica:
caratterizzare la struttura delle soluzioni
ottimali
definire ricorsivamente il valore di una
soluzione ottimale del problema
calcolare una soluzione ottimale in modo
bottom-up
dai sottoproblemi pi semplici a quelli
pi difficili, fino al problema originario
costruire una soluzione ottimale del
problema richiesto

135

Problema: taglio delle aste (1)


Il prezzo di un'asta di acciaio dipende dalla sua
lunghezza
problema: date delle aste di lunghezza n che
posso tagliare in pezzi pi corti, devo trovare il
modo ottimale di tagliare le aste per
massimizzare il ricavo che posso derivare dalla
vendita delle aste
il ricavo massimo lo potrei avere anche non
tagliando l'asta, e vendendola intera
Esempio di tabella dei prezzi:
lunghezza i

10

prezzo pi

10

17

17

20

24

30

per esempio, un'asta di lunghezza 4 pu


essere tagliata in tutti i modi seguenti (tra
parentesi il prezzo):
[4](9), [1+ 3](9), [2+2](10), [3+1](9),
[1+1+2](7), [1+2+1](7), [2+1+1](7),
[1+1+1+1](4)
il taglio ottimale in questo caso unico,
ed [2,2]

136

Problema: taglio delle aste (2)


Data un'asta di lunghezza n, ci sono 2n-1 modi
di tagliarla (secondo coordinate intere)
ho n-1 punti di taglio, se indico con una
sequenza di n-1 0 e 1 la decisione di
tagliare o no ai vari punti di taglio (per
esempio, per un'asta lunghezza 4, la
decisione di non tagliare data da 000; la
decisione di tagliare solo a met data da
010, ecc.), ogni sequenza corrisponde ad un
numero binario, e con n-1 cifre binarie
posso rappresentare fino a 2n-1 valori
Chiamiamo rn il ricavo massimo ottenibile dal
taglio di un'asta di lunghezza n
per esempio, dati i prezzi della tabella di
cui sopra abbiamo r4 = 10, mentre r10 = 30
(derivante da nessun taglio)

137

Sottostruttura ottima (1)


Per un qualunque n, la forma di rn del tipo
ri+rn-i
a meno che l'ottimo preveda di non tagliare
l'asta; in questo caso abbiamo rn = pn, il
prezzo dell'asta intera
in altre parole,
rn = max(pn, r1+rn-1, r2+rn-2, ..., rn-1+r1)
Quindi, l'ottimo dato dalla somma dei ricavi
ottimi derivanti dalle 2 semiaste ottenute
tagliando l'asta in 2
l'ottimo incorpora cio i 2 ottimi delle
soluzioni dei 2 sottoproblemi
notiamo che per forza cos: se non fosse
vero che ri ed rn-i sono gli ottimi dei
rispettivi sottoproblemi, allora, sostituendo
per esempio ad ri una soluzione ottima del
taglio di un'asta di lunghezza i otterremmo
un ricavo totale > rn, che non potrebbe
essere pi ottimo

138

Sottostruttura ottima (2)


Quando la soluzione di un problema
incorpora le soluzioni ottime dei suoi
sottoproblemi, che si possono risolvere
indipendentemente, diciamo che il
problema ha una sottostruttura ottima
Riformulando l'espressione dell'ottimo rn,
inoltre, possiamo fare dipendere rn dall'ottimo
di un solo sottoproblema:
rn = prezzo del primo pezzo tagliato +
taglio ottimo della restante asta, cio
rn = pi + rn-i
ci vale anche nel caso particolare in cui
l'asta non va tagliata; in questo caso rn
= pn + r0, con r0 = 0
Quindi, rn = max1in(pi+rn-i)

139

Algoritmo ricorsivo (1)


Applicando l'espressione ricorsiva appena vista
della soluzione del problema del taglio delle
aste otteniamo il seguente algoritmo
CUT-ROD(p,n)
1 if n = 0
2
return 0
3 q := -
4 for i := 1 to n
5
q = max(q, p[i]+CUT-ROD(p,n-i))
6 return q
n 1

T j
Tempo di esecuzione: T(n) = 1 +
cio T(n) = 2n
j=0
lo si pu vedere sostituendo la soluzione
nella ricorrenza

140

Algoritmo ricorsivo (2)

Tempo di esecuzione: 2n
Il tempo di esecuzione cos alto perch gli
stessi problemi vengono risolti pi e pi volte:

141

Algoritmo di programmazione dinamica (1)


Usando un po' di memoria extra, si riesce a
migliorare di molto il tempo di esecuzione
addirittura diventa polinomiale
trade-off spazio-temporale: aumento la
complessit spaziale, riducendo quella
temporale
Idea: memorizzo il risultato dei sottoproblemi
gi calcolati, e quando li reincontro, invece di
ricalcolarli, mi limito ad andare a prendere il
risultato dalla tabella
risolvo ogni problema distinto una volta
sola
il costo diventa polinomiale se il numero di
problemi distinti da risolvere polinomiale,
e la risoluzione dei singoli problemi
richiede tempo polinomiale

142

Algoritmo di programmazione dinamica (2)


2 tecniche per implementare la
programmazione dinamica: un metodo topdown, ed uno bottom-up
Nel metodo top-down, comincio a risolvere il
problema di dimensione n, e ricorsivamente
vado a risolvere i sottoproblemi via via pi
piccoli; aumento per l'insieme dei parametri
passati con una tabella nella quale memorizzo i
risultati gi calcolati
prima di lanciare la ricorsione sul problema
pi piccolo, controllo nella tabella se non
ho gi calcolato la soluzione
questa tecnica va sotto il nome di
memo-ization
Nel metodo bottom-up, parto dai problemi pi
piccoli, e li risolvo andando in ordine crescente
di dimensione; quando arrivo a risolvere un
problema di dimensione i, ho gi risolto tutti i
problemi di dimensioni < i

143

Versione memo-ized di CUT-ROD


MEMOIZED-CUT-ROD(p, n)
1 crea un nuovo array r[0..n]
2 for i := 0 to n
3
r[i] := -
4 return MEMOIZED-CUT-ROD-AUX(p,n,r)
MEMOIZED-CUT-ROD-AUX(p,n,r)
1 if r[n] 0
2
return r[n]
3 if n = 0
4
q := 0
5 else q := -
6
for i := 1 to n
7
q = max(q,
p[i]+MEMOIZED-CUT-ROD-AUX(p,n-i,r))
8 r[n] = q
9 return q

144

Versione bottom-up di CUT-ROD


BOTTOM-UP-CUT-ROD(p, n)
1 crea un nuovo array r[0..n]
2 r[0] := 0
3 for j := 1 to n
4
q := -
5
for i := 1 to j
6
q := max(q,p[i]+r[j-i])
7
r[j] := q
8 return r[n]
BOTTOM-UP-CUT-ROD facile vedere che
ha complessit T(n) = (n2) per i 2 cicli
annidati
Anche MEMOIZED-CUT-ROD ha complessit
T(n) = (n2), in quanto ogni sottoproblema
risolto da MEMOIZED-CUT-ROD una volta
sola, ed il ciclo 6-7 fa n iterazioni per risolvere
un problema di dimensione n
quindi si fanno in tutto n + (n-1) + (n-2) +
... + 1 iterazioni
145

Complessit di CUT-ROD con prog. din. (1)


Gli algoritmi CUT-ROD visti fino ad ora
restituiscono il massimo ricavo, ma non il
modo in cui l'asta va tagliata
Modifichiamo BOTTOM-UP-CUT-ROD per
tenere traccia non solo del massimo, ma del
modo di effettuare il taglio
EXTENDED-BOTTOM-UP-CUT-ROD(p, n)
1 crea 2 nuovi array r[0..n] e s[0..n]
2 r[0] := 0
3 for j := 1 to n
4
q := -
5
for i := 1 to j
6
if q < p[i]+r[j-i]
7
q = p[i]+r[j-i]
8
s[j] = i
9
r[j] := q
10 return (r,s)

s[j] mi dice quale la lunghezza del


primo pezzo nel taglio ottimale di un'asta di
lunghezza j
146

Complessit di CUT-ROD con prog. din. (2)


Esempio di risultato di un'esecuzione:

10

pi

10

17

17

20

24

30

r[i]

10

13

17

18

22

25

30

s[i]

10

Per stampare il taglio che mi d il ricavo massimo


uso il seguente algoritmo
PRINT-CUT-ROD-SOLUTION(p, n)
1 (r,s) := EXTENDED-BOTTOM-UP-CUT-ROD(p,n)
2 while n > 0
3
print s[n]
4
n := n - s[n]

147

Algoritmi golosi (1)

Per quanto con la programmazione dinamica


un sottoproblema non venga risolto pi di una
volta, comunque occorre analizzare diverse
soluzioni per decidere quale l'ottimo
A volte per non serve provare tutte le
soluzioni: dimostrabile che una sola pu
essere quella ottima
Questo esattamente quel che succede negli
algoritmi golosi (greedy)

148

Algoritmi golosi (2)


Il problema della scelta delle attivit:
n attivit a1,a2,..,an usano la stessa risorsa
es: lezioni da tenere in una stessa aula
Ogni attivit ai ha un tempo di inizio si ed
un tempo di fine fi con si < fi
ai occupa la risorsa nellintervallo
temporale semiaperto [si, fi)
ai ed aj sono compatibili se [si, fi) e [sj, fj)
sono disgiunti
voglio scegliere il massimo numero di
attivit compatibili
supponiamo che le attivit a1,a2,..,an
siano ordinate per tempo di fine non
decrescente f1 f2 ... fn altrimenti le
ordiniamo in tempo O(n log n)
Esempio
i

10

11

si

12

fi

10

11

12

13

14

insieme di attivit compatibili: {a3, a9, a11}


massimo numero di attivit compatibili: 4
un esempio: {a2, a4, a9, a11}
149

Soluzione del problema (1)


Possiamo risolvere il problema con un
algoritmo di programmazione dinamica
Definiamo Sij come l'insieme delle attivit che
iniziano dopo la fine di ai e terminano prima
dell'inizio di aj
quindi che sono compatibili con ai (e quelle
che non terminano dopo ai) e con aj (e
quelle che iniziano non prima di aj)
Sij = {at S : fi st < ft sj }
Chiamiamo Aij un insieme massimo di attivit
compatibili in Sij
supponiamo che ak sia una attivit di Aij,
allora Aij della forma:
Aij = Aik {ak} Akj
fatto dell'ottimo del sottoproblema Sik
pi l'ottimo del sottoproblema Akj
se cos non fosse, allora potrei trovare
un insieme di attivit pi grande di Aik
(resp. Akj), in Sik, il che vorrebbe dire
che Aij non un ottimo di Sij (assurdo)

questa dunque una sottostruttura


ottima

150

Soluzione del problema (2)


Memorizziamo in una tabella c la dimensione
dell'ottimo del problema Aij, cio c[i, j] = |Aij|
Allora abbiamo che c[i, j] = c[i, k] + c[k, j] + 1
Se non sappiamo che la soluzione ottima include
l'attivit ak, dobbiamo provare tutte le attivit in
Sij, cio
c[i, j] = 0
se Sij =
= maxak Sij {c[i, k] + c[k, j] + 1} se Sij
esercizio per casa: scrivere gli algoritmi di
programmazione dinamica con memoization e
con tecnica bottom-up

151

Algoritmo goloso
E' inutile per provarle tutte per risolvere il problema
Sij, sufficiente prendere l'attivit a1 che finisce per
prima in Sij, e risolvere il problema Skj, con k prima
attivit in Sij che inizia dopo la fine di a1
se chiamiamo Sk l'insieme di tutte le attivit che
iniziano dopo la fine di ak, cio Sk = {at S : fk st},
dopo che abbiamo preso a1, ci rimane da risolvere il
solo problema S1
Abbiamo il seguente risultato:
dato un sottoproblema Sk, se am l'attivit che finisce
per prima in Sk, am inclusa in qualche sottoinsieme
massimo di attivit mutuamente compatibili di Sk
supponiamo che Ak sia un sottoinsieme massimo di
Sk, e chiamiamo aj l'attivit che finisce per prima in
Ak; allora o aj = am, oppure fm fj, e se sostituisco aj
con am in Ak ho ancora un sottoinsieme massimo A'k
di Sk
Quindi, per risolvere il problema di ottimizzazione mi
basta ogni volta scegliere l'attivit che finisce prima,
quindi ripetere l'operazione sulle operazioni che
iniziano dopo quella scelta

152

Pseudocodice (1)
Versione ricorsiva
s e f sono array con, rispettivamente, i
tempi di inizio e di fine delle attivit
k l'indice del sottoproblema Sk da
risolvere (cio l'indice dell'ultima attivit
scelta
n la dimensione (numero di attivit) del
problema originario

RECURSIVE-ACTIVITY-SELECTOR(s,f,k,n)
1 m := k + 1
2 while m n and s[m] < f[k]
3
m := m + 1
4 if m n
5 return {am}
RECURSIVE-ACTIVITY-SELECTOR(s,f,m,n)
6 else return

153

Pseudocodice (2)
Versione iterativa:
GREEDY-ACTIVITY-SELECTOR(s,f)
1 n := s.length
2 A := {a1}
3 k := 1
4 for m := 2 to n
5
if s[m] f[k]
6
A := A {am}
7
k := m
8 return A

entrambe hanno complessit (n), in


quanto considerano ogni attivit una volta
sola

154

Alcune domande (e risposte) finali (1)


(con uno sguardo allindietro)
Esiste una sorta di classe universale di
complessit
cio esiste una qualche funzione di
complessit T(n) tale che tutti i
problemi risolvibili impiegano al pi
T(n)?
Il nondeterminismo pu cambiare la
complessit di soluzione dei problemi?
in primis, come si definisce la
complessit di un modello
nondeterministico?

155

Alcune domande (e risposte) finali (2)

Cominciamo con alcune definizioni/richiami,


per fissare le idee
Data una funzione T(n), indichiamo con
DTIME(T) l'insieme dei problemi tali che
esiste un algoritmo che li risolve in tempo T(n)
Pi precisamente:
problema = riconoscimento di un
linguaggio
per semplicit, consideriamo i linguaggi
ricorsivi
algoritmo = macchina di Turing
(Ma quando T un polinomio )
Riformulando: DTIME(T) (risp. DSPACE(T))
la classe (l'insieme) dei linguaggi (ricorsivi)
riconoscibili in tempo (risp. spazio) T mediante
macchine di Turing deterministiche a k nastri
di memoria

156

Alcune domande (e risposte) finali (3)

Un primo risultato: data una funzione totale e


computabile T(n), esiste un linguaggio
ricorsivo che non in DTIME(T)
c' quindi una gerarchia di linguaggi
(problemi) definita sulla base della
complessit temporale deterministica
una cosa analoga vale per DSPACE, e
per le computazioni nondeterministiche
(NTIME ed NSPACE)
Lo schema di dimostrazione ricalca
quello dellindecidibilit del problema
della terminazione del calcolo
a proposito...

157

Computazioni nondeterministiche
(richiami)
Data una macchina di Turing nondeterministica M,
definiamo la sua complessit temporale TM(x) per
riconoscere la stringa x come la lunghezza della
computazione pi breve tra tutte quelle che accettano x
TM(n) poi (nel caso pessimo) il massimo tra tutti i
TM(x) con
|x| = n
Quindi NTIME(T) la classe (l'insieme) dei linguaggi
(ricorsivi) riconoscibili in tempo T mediante macchine
di Turing nondeterministiche a k nastri di memoria
Tantissimi problemi si risolvono in modo molto naturale
mediante meccanismi nondeterministici (per esempio,
trovare un cammino in un grafo che tocca tutti i nodi)...
... per i meccanismi di computazione reali sono
deterministici
se riuscissimo a trovare una maniera poco onerosa per
passare da una formulazione nondeterminisitca ad una
deterministica, tantissimi problemi interessanti
potrebbero essere risolti (in pratica) in modo
(teoricamente) efficiente
per spesso abbiamo notato una esplosione nel
passaggio da un meccanismo ND ad uno D (quando
i 2 meccanismi sono equipotenti, come peraltro il
caso delle MT)
per esempio, esplosione del numero degli stati
nel passare da NDFSA a DFSA
158

Relazione tra DTIME e NTIME (1)


Sarebbe utile poter determinare, date certe
interessanti famiglie di funzioni di
complessit, se la classe dei problemi
risolvibili non cambia nel passare da
computazioni deterministiche a quelle
nondeterministiche
in altri termini, se DTIME() = NTIME()
per certe famiglie ={Ti}, di funzioni
Ad esempio .
Una fondamentale classe di problemi:
P = i1 DTIME(ni)
convenzionalmente, questi sono considerati
i problemi trattabili
Similmente:
NP = i1 NTIME(ni)
Altre classi interessanti di problemi: PSPACE,
NPSPACE, EXPTIME, NEXPTIME,
EXPSPACE, NEXPSPACE

159

Relazione tra DTIME e NTIME (2)

LA domanda: P = NP?
boh...
probabilmente no, ma non si ancora
riusciti a dimostrarlo
Pi in generale:
LogSpace P NP PSPACE;
LogSpace PSPACE
Ma
Alcuni esempi di problemi della classe NP:
Soddisfacibilit di formule di logica
proposizionale (SAT): data una formula F
di logica proposizionale, esiste un
assegnamento dei valori alle lettere
proposizionali che compaiono in F tale che
F vera?
detto in altro modo: F ammette un
modello?
Circuito hamiltoniano (HC): dato un grafo
G, esiste un cammino in G tale che tutti i
nodi del grafo sono toccati una ed una sola
volta prima di tornare al nodo di partenza?
160

Riduzione in tempo polinomiale e completezza (1)


Un linguaggio (problema) L1 riducibile in
tempo polinomiale ad un altro linguaggio L2 se
e solo se esiste una MT deterministica
(traduttrice) con complessit in P che per ogni
x produce una stringa (x) tale che (x) L2 se
e solo se x L1

Se una classe di linguaggi, diciamo che


un linguaggio L (che non detto che debba
essere in ) -difficile rispetto alle riduzioni
in tempo polinomiale se e solo se, per ogni
L' , L' riducibile in tempo polinomiale a L
cio se risolvere L (determinare se una
stringa x appartiene ad L o no) almeno
tanto difficile quanto risolvere un
qualunque linguaggio in
Un linguaggio L -completo se -difficile
ed in
se si trovasse un problema NP-completo che
risolvibile in tempo polinomiale, allora
avremmo P = NP
dualmente, se si trovasse un problema NPcompleto che non risolvibile in tempo
polinomiale, allora avremmo P NP
161

Riduzione in tempo polinomiale e completezza (2)


SAT NP-difficile
quindi NP-completo
si mostra codificando le computazioni di
una generica MT nondeterministica M (con
complessit polinomiale) in SAT, in modo
che M accetti una stringa x se e solo se una
opportuna formula s soddisfacibile
HC anch'esso NP-difficile (e NP-completo)
NP-completezza di HC si mostra riducendo
SAT a HC
tantissimi altri problemi sono NP-completi...
Oggigiorno per
NP-completezza non pi sinonimo di
intrattabilit pratica
Il giochino di ridurre un problema a SAT
(magari finitizzandolo) molto di moda
Si apre una nuova frontiera pratico teorica
.

162

SAT NP-difficile
Come ridurre un generico problema NP a
SAT in tempo polinomiale
(deterministicamente)
Lidea base tutto sommato non
particoramente complicata (come tutte le
grandi idee ):
Fornire una computazione deterministica che,
per ogni MT non deterministica M con
complessit polinomiale p e per ogni stringa
x sullalfabeto di M, tale che |x| = n, produca
come uscita una formula proposizionale W, in
tempo polinomiale p(n), tale che W sia
soddisfacibile se e solo se x L(M).

163

Se la MT ha complessit p(n), la formula


complessiva proposizionale ha dimensione
p2(n) e la traduzione da MT a formule
proposizionali avviene in tempo pure
polinomiale.
Il nocciolo della dimostrazione deriva
dallosservazione che, se x L(M), allora
esiste una sequenza di mosse, la cui
lunghezza non eccede p(n), che conduce ad
uno stato di accettazione. Quindi, al pi p(n)
+ 1 celle di memoria possono essere
utilizzate da M durante una simile
computazione. Una volta compreso che una
configurazione di una MT ha uno spazio
limitato, la si pu descrivere mediante
unopportuna formula proposizionale.
Ad esempio, una variabile logica Qt,k pu
stabilire se, allistante t, lo stato di M sia qk.
Unaltra variabile, Ct,i,k, pu stabilire se,
allistante t, la cella i-esima contenga il
simbolo ak e cos via. Inoltre, opportuni
connettivi logici possono imporre che,
allistante t = 0, la formula descriva una
configurazione iniziale, che la configurazione
allistante t + 1 derivi dalla configurazione
allistante t e che, allistante t = p(n), M si
trovi in uno stato di accettazione.
164

Traduzione in SAT di una generica MT ND a


complessit P(n)
M a nastro singolo, con nastro lmitato a
sinistra senza perdita di generalit. Perch ?
La formula W viene costruita come
congiunzione ( logico) di diverse
sottoformule, o clausole, secondo le seguenti
regole. Sia t la variabile tempo, con
0 t p(n), e sia i la posizione di una cella di
memoria, 0 i p(n).

165

1. Descrizione della configurazione di M.


Si definiscono i seguenti insiemi di variabili
logiche.
{Qt,k| 0 t p(n), 0 k |Q| 1}
Qt,k risulter vera se e solo se M si trover nello
stato qk allistante t.
{Ht,i| 0 t p(n), 0 i p(n)}
Ht,i risulter vera se e solo se la testina M si
trover nella posizione i allistante t.
{Ct,i,h| 0 t p(n), 0 i p(n), 0 h |A| 1,
dove A lalfabeto completo di M}
Ct,i,h risulter vera se e solo se, allistante t, la
cella i-esima conterr il simbolo ah.
Abbiamo in questo modo definito un numero
g (n ) (p(n ) 1) | Q | (p(n ) 1) 2 (p(n ) 1) 2 | A |

di variabili logiche, dove g una funzione


(p2). In breve, diremo che si sono definite
(p2(n)) variabili.
Chiaramente, in ogni istante, M deve trovarsi
esattamente in uno stato, la sua testina deve
essere esattamente sopra una cella e ciascuna
cella deve contenere esattamente un simbolo.
In questo modo si ottiene un primo gruppo di
clausole (clausole di configurazione, CC) che
le variabili logiche devono soddisfare per
descrivere le configurazioni di M.

166

Clausole di configurazione (CC) (1):

Qt ,k

0t p ( n ) 0 k |Q|1

{M deve trovarsi in almeno uno stato alla volta}

t , k1

0t p ( n )
0 k1 k 2 |Q|1

Qt ,k2

{Ad ogni istante M non pu trovarsi in due stati


diversi}

H t ,i

0t p ( n ) 0i p ( n )

0t p ( n )
0i , j p ( n )
i j

t ,i

H t , j

{La testina di M si trova esattamente su di una


cella alla volta}
167

Clausole di configurazione (CC) (2):

Ct ,i ,h

0t p ( n ) 0 h | A|1

0i p ( n )

0t p ( n )
0i p ( n )
0 h , k | A|1
hk

t ,i , h

Ct ,i ,k

{In ogni istante, ciascuna cella contiene esattamente


un simbolo}
Riassumendo, CC contiene il seguente numero di
clausole:
(p(n) + 1) |Q| + (p(n) + 1) |Q| (|Q| 1) +
+ (p(n) + 1)2 + (p(n) + 1)2 p(n) +
+ (p(n) + 1)2 |A| + (p(n) + 1)2 |A| (|A| 1)
dove ciascuna clausola lor logico di un numero
limitato a priori di letterali, e ciascun letterale o
una variabile logica o la sua negazione. Quindi, CC
contiene (p3(n)) clausole e (p3(n)) letterali (si
ricordi che |Q| e |A| sono costanti).
168

Descrizione della configurazione iniziale.

Allistante t = 0, M deve trovarsi in q0, la sua testina


deve trovarsi nella posizione 0 e x = ak0ak1...ak(n 1)
deve essere immagazzinata nelle prime n celle del
nastro. Tutte le restanti celle devono risultare vuote.
Ci produce un secondo gruppo di clausole
(Clausole della configurazione iniziale, IC), qui
elencate.

Q0,0 H 0,0 C0,0,k0 C0,n1,kn1

n i p ( n )

C0,i,0

(si suppone che il blank sia a0)


IC ha (p(n)) letterali.

169

Descrizione della configurazione di accettazione.


Si adotti la convenzione per la quale, se M si ferma
allistante tH < p(n), allora mantiene la
configurazione finale per tutti gli istanti t,
tH t p(n). Questa convenzione provoca
conseguenze sulle transizioni della macchina che
verranno descritte fra poco, ma consente di stabilire
che, allistante t = p(n), M deve trovarsi in uno stato
di accettazione. Ci produce unaltra clausola
(Clausola di accettazione, AC) :

Qp ( n),i1 Qp ( n),iS
dove {qi1,...,qis} = F
AC ha (1) letterali.

170

Descrizione della relazione di transizione.(1)


La funzione di transizione sia del tipo
(qk,sh) = {qk,sh,N}, con la convenzione che, ogni
volta che la originale di M risulta indefinita per
qualche qk e per qualche sh, si pone (qk,sh) =
{qk,sh,S}. Indichiamo con m la cardinalit di
{qk,sh,N}.
Per ogni t e per ogni i, se M si trova nello stato qk, se la sua testina
nella posizione i e se sta leggendo sh, allora, allistante t + 1,
M si trova, in modo mutuamente esclusivo, in una
configurazione in cui lo stato qk, la posizione i memorizza
sh e la sua testina si trova nella posizione i, oppure nella i + 1,
oppure nella i 1, in dipendenza da N, se e solo se la terna
qk,sh,N appartiene a (qk, sh).

0t p ( n )
0i p ( n )
0 k |Q|1
0 h | A|1

t ,k

H t ,i Ct ,i ,h Qt 1,k ' H t 1,i ' Ct 1,i ,h '

Qt 1,k '' H t 1,i'' Ct 1,i,h''

...

Qt 1,k m H t 1,im Ct 1,i,hm


qk ' , sh ' , N ' , qk '' , sh '' , N ' ' ,..., qk m , sh m , N m d (qk , sh )
171

Trasformata in forma disgiuntiva


pesantuccio ma utile

0t p ( n )
0i p ( n )
0 k |Q|1
0 h| A|1

t ,k

t ,k

t ,k

H t ,i Ct ,i ,h Qt 1,k ' Qt 1,k m

Ht ,i Ct ,i,h Qt 1,k ' Qt 1,k m1 Ht 1,hm


H t ,i Ct ,i,h Qt 1,k ' H t 1,hm1 Qt 1,k m

(vengono considerate tutte le congiunzioni formate


dalle possibili disgiunzioni di Qt+1, k, Ht+1,h e Ct+1,i,h,
prendendo uno e un solo elemento da ciascun
termine in or esclusivo, e vengono combinate con
lantecedente)

172

Descrizione della relazione di transizione.(2)


Se, allistante t, la testina si trova nella posizione i,
allora, allistante t + 1, tutte le celle, esclusa la iesima, hanno lo stesso contenuto che avevano
allistante t.

0t p ( n )
0i p ( n )
0 k |Q|1
0 h | A|1

t ,i

Ct ,i ,h Ct 1,i ,h

che, ancora, si pu riformulare nel modo seguente:

H t ,i Ct ,i ,h Ct 1,i ,h'

173

Linsieme TC di clausole relative alla transizione


definito come la congiunzione delle formule
derivate in 4.1 e 4.2. TC ha (p2(n)) letterali
(si noti che il numero di nelle clausole 4.1
limitato a priori perch cos la cardinalit degli
insiemi {qk,sh,N} e, di conseguenza, il numero
totale di clausole viene moltiplicato nella
trasformazione in forma normale congiuntiva per un
termine che dipende pur esponenzialmente solo
da m).

Si pone, infine, W = CC IC AC TC, che


significa che W la congiunzione di tutte le
clausole che appartengono agli insiemi precedenti.
W ha quindi (p3(n)) letterali.

174

Osservazioni conclusive
C0 CA, dove C0 la configurazione iniziale e CA
una configurazione di accettazione per M, se e solo
se W risulta soddisfacibile.
Una formula proposizionale che contiene n occorrenze
di variabili logiche si pu codificare come una
stringa di lunghezza (nlog n). Quindi, per ogni x
di lunghezza n, la sua traduzione (x) in una stringa
che codifica W certamente non pi lunga di
(p4(n)). Una volta compreso che la traduzione
pu essere eseguita da una MT deterministica
(anche multinastro) in un tempo (|(x)|), la
riducibilit in tempo polinomiale completamente
dimostrata.
Non bisogna effettivamente conoscere una MT M che
risolva il problema originale. Lesistenza di tale
macchina garantisce lesistenza di una macchina
riducente in tempo polinomiale. Tuttavia, se si
conosce una simile macchina M e se noto un
limite di tempo polinomiale p per la sua
computazione nondeterministica, allora la
dimostrazione consente effettivamente di costruire
la macchina che esegue la riduzione dei problemi.
Corollario
Il problema di stabilire la soddisfacibilit delle formule
proposizionali in forma normale congiuntiva NP 175
completo.

Il problema del cammino Hamiltoniano (HC)


NP-difficile
Ridurremo SAT a HC (non viceversa!): SAT ha fatto da
apripista verso la NP-completezza: la sua natura
riflette al meglio la generalit del concetto.
Per vale anche il viceversa.
Ne quindi una naturale conseguenza il fatto che esso
possa essere ridotto in modo abbastanza naturale a
moltissimi altri problemi implicandone la NPdifficolt (e completezza se )
Tutto ci sottolinea la natura forte dellNPcompletezza che in un certo senso ripropone
allinterno della categoria dei problemi decidibili,
lapproccio che ha portato alla semidecidibilit, ossia:
non so fare di meglio che enumerare le possibili
soluzioni del problema (finite, infinite, esponenziali)
nella dimensione del problema, e verificare se
effettivamente sono tali.
Tornando a SAT HC sfrutteremo il corollario
precedente, ossia partiremo da una versione di SAT in
forma normale:
Per ogni formula proposizionale W in forma normale
congiuntiva, costruiremo un grafo G che ammette un
HC se e solo se W risulta soddisfacibile.
176

Intuizione ed esempio
Il procedimento consiste nel costruire G come
aggregazione di due classi di pezzi. I pezzi del
primo tipo saranno associati a ciascuna variabile
logica in W e, in un certo senso, ne mostreranno
tutte le occorrenze. I pezzi del secondo tipo saranno
associati a ciascuna clausola di W e legati ai nodi
che appartengono ai pezzi del primo tipo, in modo
tale che il loro attraversamento garantisca la verit
della clausola associata.

177

W: A1 ( A1 A2)

NB: L21 = A1

178

Pi in generale:

179

pi il massimo numero fra il numero di occorrenze


di Ai e quelle di Ai in W
Per ogni r, se Ljr Ajr, si connetta il primo nodo Fjr,s
di GAjr avente solo due archi uscenti a Ljr, e L jr a
Tjr,s+1. Viceversa se Ljr Ajr.
(possibilie poich pi maggiore o uguale al numero
di occorrenze di Ai in W.)
Se i GCj vengono ignorati, vi sono esattamente 2m
HC nel grafo restante, . Ci corrisponde al fatto che,
se W vuota ogni assegnamento di valori di verit,
banalmente, la soddisfa.
Un possibile HC entrante in qualche GCj da Ljr deve
lasciarlo da L. jr . Ad esempio, un cammino che
entra in GCj da Lj2, visitando L j 2 , L j1 e
lasciando infine GCj, renderebbe Lj1 inaccessibile.
Ecc.
Ci non implica che un eventuale HC debba visitare
tutti i nodi di ciascun GCj consecutivamente.

180

Ogni possibile HC deve essere ottenuto da un HC di


sostituendo qualche arco del tipo Fi,s, Ti,s+1 con un
cammino che passa attraverso qualche GCj, se HC
contiene larco IAi, Tj0. Viceversa, se HC contiene
larco IAi, Fi0 , esso deve essere ottenuto da un HC
di , sostituendo qualche arco del tipo Ti,s, Fi,s+1
con un cammino che passa attraverso qualche GCj.
Ergo, si pu entrare in ogni GCj attraverso qualche
Ljr mediante un HC, solo da qualche Tjr,s se IAjr,
Fjr,0 in HC e solo da qualche Fjr,s se IAjr, Tjr,0
in HC:
entrare in GCj da qualche Fjr,s significa soddisfarlo
supponendo Ajr = T.

181

Riassumendo
I modelli
Il calcolo
Lanalisi e la sintesi di algoritmi
Inventati da capire e analizzare
Inventati da catalogare
Da inventare o soltanto da
Applicare/adattare

182

Riassumendo:
a voi la parola
(grazie per la pazienza)

183

Você também pode gostar