Lezione12 - Dipartimento di Ingegneria dell`Informazione

Lezione 12
B-Alberi
Algoritmi su grafi
Sommario
• B-Alberi
– definizione
– ricerca
– inserimento
• Rappresentazione dei grafi
– Visita in ampiezza
– Visita in profondità
• Ordinamento topologico
B-Alberi
• I B-Alberi sono una generalizzazione degli alberi
binari di ricerca
• la principale differenza è che i B-Alberi
– ogni nodo dell’albero può contenere n>2 chiavi
– il grado di un nodo è alto (50-2000)
• i B-Alberi sono utilizzati per garantire l’efficienza delle
operazioni su insiemi dinamici (ricerca, inserzione e
cancellazione) di dati memorizzati su supporti
secondari (dischi)
Visualizzazione
M
DH
BC
FG
QTX
JKL
NP
RS
VW
YZ
Memorie Secondarie
• La memoria primaria (RAM) si basa su una
tecnologia costosa ma che permette di eseguire le
operazioni di scrittura e lettura in modo veloce
• la memoria secondaria (dischi) è più lenta (vi sono
componenti meccaniche da muovere), ma più
economica
• questo permette di rendere disponibile una quantità
di memoria secondaria di uno o due ordini di
grandezza maggiore della memoria primaria
Memorie Secondarie
• Le informazioni in un disco sono organizzate in
blocchi
• il blocco minimo accessibile in lettura e scrittura è
detto pagina
• una pagina corrisponde a circa 2 MB
Accesso alla memoria secondaria
• Per trattare quantità estremamente grandi di dati si
devono pertanto sviluppare algoritmi che lavorino con
dati memorizzati in memoria secondaria
• si devono pertanto minimizzare gli accessi alla
memoria oltre che garantire efficienza
computazionale di CPU
• Le operazioni di accesso ai dati negli algoritmi
vengono modificate in:
x=puntatore a un dato
Disk-Read(x)
…operazioni di elaborazione di x
Disk-Write(x)
…operazioni che accedono a x in sola lettura
Accesso alla memoria secondaria
• Le operazioni di lettura su disco si intendono fatte nel
caso in cui il dato puntato da x non sia già disponibile
nella memoria primaria
• le operazioni di scrittura vengono invece eseguite
solo se il dato puntato da x è stato in qualche modo
modificato
B-Alberi
• In ogni istante è possibile mantenere in memoria
primaria solo un numero limitato di pagine
• le operazioni eseguite su i B-Alberi garantiscono di
poter essere eseguite conservando solo un numero
costante di pagine in memoria principale (tante più
pagine tanto più efficienti saranno le varie operazioni)
• in genere un nodo di un B-Albero e tanto grande
quanto una pagina di memoria secondaria
• Nota: nel presentare gli algoritmi si trascurerà la
gestione di basso livello della memoria
B-Alberi
• Per semplicità si suppone di memorizzare in un nodo
solo la chiave dei dati
• un eventuale puntatore associato alla chiave servirà
per indirizzare la pagina del disco su cui trovare i dati
satellite
Definizione dei B-Alberi
• un B-Albero è un albero radicato T che soddisfa le
seguenti proprietà:
– ogni nodo x è caratterizzato dai seguenti attributi:
• n[x] numero delle chiavi memorizzate in x
• le n[x] chiavi sono memorizzate in ordine decrescente
• leaf[x] è true se il nodo è una foglia, false altrimenti
– un nodo interno x contiene n[x]+1 puntatori c1[x], c2[x],…,
cn[x]+1[x] ai suoi figli (o NIL se x è una foglia)
– i campi keyi[x] definiscono gli intervalli delle chiavi
memorizzate in ciascun sottoalbero: se ki è una qualunque
chiave memorizzata nel sottoalbero di radice ci[x] allora
k1  key1[x]  k2  key2[x]  …  keyn[x][x]  kn[x]+1
– tutte le foglie sono alla stessa profondità, che coincide con
l’altezza dell’albero
Definizione dei B-Alberi
• il numero delle chiavi per ogni nodo è limitato sia
inferiormente che superiormente in funzione di un
intero t chiamato grado minimo del B-Albero
–
–
–
–
–
–
t2
ogni nodo (eccetto la radice) contiene almeno t-1 chiavi
ogni nodo interno (eccetto la radice) ha almeno t figli
ogni nodo può contenere al massimo 2t-1 chiavi
ogni nodo interno può avere al massimo 2t figli
un nodo è detto pieno se contiene esattamente 2t-1 chiavi
Altezza di un B-Albero
• Un B-Albero con n chiavi e grado minimo t ha una
altezza h  logt (n+1)/2
• Infatti: il caso peggiore è che un B-Albero abbia una
radice con un’unica chiave e che tutti i nodi
contengano il numero minimo di chiavi, cioè t-1
• a profondità 1 ci saranno pertanto 2 nodi, a
profondità 2, 2t nodi, a profondità 3, 2t2 nodi.
• Ogni nodo contiene t-1 chiavi
• pertanto il numero totale di chiavi n deve essere:
• n  1 + (t-1)i=1..h 2ti-1= 1+2(t-1)(th-1)/(t-1)=2th-1
• ovvero h  logt (n+1)/2
Operazioni sui B-Alberi
• La radice del B-Albero è sempre in memoria
principale
– non devono pertanto essere effettuate operazioni di DiskRead per leggere la radice
– tuttavia se si modifica la radice deve essere eseguita una
operazione di Disk-Write
• si suppone che per tutti i nodi passati come
parametro alle varie procedure si sia correttamente
compiuta l’operazione di Disk-Read
• tutte le procedure che vedremo sono a “singola
passata” cioè algoritmi che visitano l’albero a partire
dalla radice e non risalgono mai indietro
Ricerca
• E’ un operazione simile alla ricerca sugli alberi binari
di ricerca
• la differenza è che non ci sono solo due vie possibili
ad ogni nodo, ma n[x]+1
• la procedura B-Tree-Search
– prende in ingresso il puntatore alla radice dell’albero e la
chiave da cercare
– restituisce la coppia ordinata (y,i) che consiste di un
puntatore a nodo y e un indice i tale che keyi[y]=k
Pseudocodice per la Ricerca
B-Tree-Search(x,k)
1 i  1
2 while i  n[x] e k > keyi[x]
3 do
i  i+1
4 if i  n[x] e k = keyi[x]
5 then return (x,i)
6 if leaf[x]
7 then return NIL
8 else DISK-READ(ci[x])
9
return B-Tree-Search(ci[x],k)
Spiegazione pseudocodice
• Nelle linee 1-3 si esegue una ricerca lineare per
trovare il più piccolo indice i tale che k  keyi[x]
• in 4-5 si controlla se la chiave è stata trovata
• altrimenti 6-9 o siamo in una foglia e la ricerca
termina senza successo
• o procediamo ricorsivamente su un opportuno
sottoalbero del nodo in esame che contiene chiavi
comprese fra un valore sicuramente più piccolo di k e
uno più grande
Visualizzazione
Ricerca della chiave R
M
DH
BC
FG
QTX
JKL
NP
RS
VW
YZ
Analisi
• La ricerca procede dalla radice lungo un cammino
verso una foglia
• il numero di accessi è pertanto O(h)=O(logtn)
• poiché il numero di chiavi in un nodo è n[x]<2t la
ricerca lineare 2-3 impiega per esaminare un
qualsiasi nodo un tempo O(t)
• il tempo complessivo sarà pertanto O(t logtn)
Costruzione di un B-Albero
• Per costruire un B-Albero si utilizza una procedura BTree-Create per creare un nodo radice vuoto
• poi si utilizza la procedura B-Tree-Insert per inserire
ogni nodo
• entrambe queste procedure fanno uso di una
procedura ausiliaria Allocate-Node() che ha il compito
di creare un nuovo nodo e di assegnargli una
opportuna pagina del disco in tempo O(1)
Pseudocodice per la costruzione
della radice di un B-Albero
B-Tree-Create
1 x  Allocate-Node()
2 leaf[x]  true
3 n[x]  0
4 Disk-Write(x)
5 root[T]  x
Divisione di un nodo in un B-Albero
• L’operazione di inserzione di un nodo è complicata
dal fatto che se la nuova chiave deve essere
memorizzata in un nodo pieno allora bisogna
procedere a dividere questo nodo in due
• un nodo pieno y con 2t-1 chiavi viene diviso in due
nodi di t-1 chiavi all’altezza della chiave mediana
keyt[y]
• la chiave mediana viene spostata nel nodo padre
• se y è la radice si aumenta l’altezza dell’albero: è
infatti questo il meccanismo di crescita dei B-Alberi
Visualizzazione
keyi-1[x]
keyi[x]
keyi[x] keyi+1[x]
x … N W ….
y=ci[x] P Q R S T U V
… N S W ….
PQR
TUV
y=ci[x]
z=ci+1[x]
Idea intuitiva
• La procedura ha come parametri un nodo interno x
non pieno, un indice i e un nodo y pieno.
• y è il figlio i-esimo di x.
• In origine y ha 2t-1 chiavi, dopo la divisione rimane
con i t-1 chiavi minori
• un nuovo nodo z acquisisce i t-1 chiavi maggiori e
diventa un figlio di x dopo y
• la chiave mediana di y viene rimossa da y e posta in
x e diventa la chiave che separa y da z
Divisione di un nodo
B-Tree-Split-Child(x,i,y)
1 z  Allocate-Node()
2 leaf[z]  leaf[y]
3 n[z]  t-1
4 for j  1 to t-1
5 do
keyj[z]  keyj+t[y]
6 if not leaf[y]
7 then for j  1 to t
8
do
cj[z]  cj+t[y]
9 n[y]  t-1
10 for j  n[x]+1 downto i+1
11 do
cj+1[x]  cj[x]
12 cj+1[x]  z
13 for j  n[x] downto i
14 do
keyj+1[x]  keyj[x]
15 keyi[x]  keyt[y]
16 n[x]  n[x]+1
17 Disk-Write(y); Disk-Write(z); Disk-Write(x)
Spiegazione dello pseudocodice
• Le linee 1-8 creano un nuovo nodo z e gli assegnano
le t-1 chiavi più grandi di y, assieme ai figli
corrispondenti
• in 10-14 si inserisce z come nuovo figlio di x
• in 15 si inserisce la chiave mediana di y come
separatore
• in 16 si modifica il contatore delle chiavi n[x]
• in 17 si riporta su disco le modifiche effettuate
Analisi
• Il tempo di esecuzione è dominato dai cicli alle linee
4 o 7 o 10 o 13 che impiegano tutti un tempo limitato
superiormente da O(t)
Inserimento di una nuova chiave
• L’inserimento di una nuova chiave può avvenire in
due casi:
– quando il nodo radice è pieno
– quando il nodo radice non è pieno
• La procedura B-Tree-Insert inserisce una nuova
chiave k in un B-Albero e gestisce il caso in cui si
debba inserire la chiave in una radice piena
• in questo caso si aumenta di 1 l’altezza dell’albero
inserendo una nuova radice
• ci si riporta così al caso di inserimento in un albero
con radice non piena che viene trattato dalla
procedura B-Tree-Insert-Nonfull
Visualizzazione
root[T]
s
root[T]
r
ADFHLNP
r
ADF
H
LNP
Pseudocodice per l’inserimento di una
nuova chiave
B-Tree-Insert(T,k)
1 r  root[T]
2 if n[r] = 2t-1
3 then s  Allocate-Node()
4
root[T]  s
5
leaf[s]  false
6
n[s]  0
7
c1[s]  0
8
B-Tree-Split-Child(s,1,r)
9
B-Tree-Insert-NonFull(s,k)
10 else B-Tree-Insert-NonFull(r,k)
Inserimento in nodo non pieno
• La procedura è organizzata in modo tale da essere
chiamata sempre solo su nodi non pieni
• la procedura distingue il caso in cui si debba inserire
la nuova chiave in un nodo foglia o si debba
scendere ricorsivamente in un nodo interno
• per un nodo foglia si deve gestire la collocazione
della chiave nella giusta posizione e aggiornare il
numero di chiavi
• per un nodo interno si deve verificare che questo non
sia pieno per poter applicare ricorsivamente la BTree-Insert-Nonfull
• nel caso in cui sia un nodo pieno si richiama la
procedura B-Tree-Split-Child
Pseudocodice per l’inserimento
B-Tree-Insert-Nonfull(x,k)
1 i  n[x]
2 if leaf[x]
3 then while i1 e k<keyi[x]
4
do
keyi+1[x]  keyi[x]
5
i  i-1
6
keyi+1[x]  k
7
n[x]  n[x]+1
8
Disk-Write(x)
9 else while i1 e k<keyi[x]
10
do
i  i -1
11
i  i+1
12
Disk-Read(ci[x])
13
if n[ci[x]]=2t-1
14
then
B-Tree-Split-Child(x,i,ci[x])
15
if k > keyi[x]
16
then
i  i+1
17
B-Tree-Insert-Nonfull(ci[x],k)
Spiegazione pseudocodice
• In 3-8 ci si occupa del caso di inserimento della
chiave nel nodo foglia: si determina la posizione della
chiave facendole contemporaneamente posto
• in 9-17 si considera il caso in cui si debba scendere
ricorsivamente attraverso nodi interni
• in 9-11 si determina quale figlio esaminare
• in 13 se il figlio è pieno si divide e in 15-16 si
determina per quale dei due nuovi sotto figli si debba
proseguire
• in 17 si procede ricorsivamente su un nodo figlio
sicuramente non pieno fino a raggiungere una foglia
Visualizzazione inserzione della
chiave B in B-Albero con t=3
GMPX
ACDE
JK
NO
RSTUV
YZ
RSTUV
YZ
GMPX
ABCDE
JK
NO
Visualizzazione
inserzione della chiave Q
GMPX
ABCDE
JK
NO
RSTUV
YZ
GMPTX
ABCDE
JK
NO
QRS
UV
YZ
Visualizzazione
inserzione della chiave L
GM PTX
ABCDE
JK
NO
QRS
UV
YZ
UV
YZ
P
GM
ABCDE
LJK
NO
TX
QRS
Visualizzazione
inserzione della chiave F
P
GM
ABCDE
LJK
TX
NO
QRS
UV
YZ
UV
YZ
P
CGM
AB
DEF
LJK
NO
TX
QRS
Analisi
• Per un B-Albero di altezza h la procedura B-TreeInsert effettua O(h) accessi al disco
• infatti:
– questa richiama la procedura B-Tree-Insert-Nonfull
ricorsivamente su un numero di nodi al più numeroso come il
massimo cammino fino ad una foglia (h)
– inoltre la procedura B-Tree-Insert-Nonfull esegue un numero
O(1) di operazioni di lettura-scrittura
Analisi
• per il tempo di computazione di CPU si ha che BTree-Insert-Nonfull ha un ciclo O(t) (linea 3 o 9)
• inoltre richiama una volta la procedura B-Tree-SplitChild che costa O(t)
• dato che B-Tree-Insert-Nonfull viene chiamata
ricorsivamente al più O(h) volte si ha
complessivamente un costo O(th)=O(t logtn)
Grafi
• I grafi sono strutture dati molto diffuse in informatica
• Vengono utilizzati per rappresentare reti e
organizzazioni dati complesse e articolate
• Per elaborare i grafi in genere è necessario visitarne
in modo ordinato i vertici
• Vedremo a questo proposito due modi fondamentali
di visita: per ampiezza e per profondità
Nota sulla notazione asintotica
• Il tempo di esecuzione di un algoritmo su un grafo
G=(V,E) viene dato in funzione del numero di vertici
|V| e del numero di archi |E|
• Utilizzando la notazione asintotica adotteremo la
convenzione di rappresentare |V| con il simbolo V e
|E| con E: quando diremo che il tempo di calcolo è
O(E+V) vorremo significare O(|E|+|V|)
Rappresentazione di un grafo
• Vi sono due modi per rappresentare un grafo:
– collezione di liste di adiacenza
– matrice di adiacenza
• si preferisce la rappresentazione tramite liste di
adiacenza quando il grafo è sparso, cioè con |E|
molto minore di |V|2
• si preferisce la rappresentazione tramite matrice di
adiacenza quando, al contrario, il grafo è denso o
quando occorre alta efficienza nel rilevare se vi è un
arco fra due vertici dati
Liste di adiacenza
• Si rappresenta un grafo G=(V,E) con un vettore Adj di
liste, una lista per ogni vertice del grafo
• per ogni vertice u, Adj[u] contiene tutti i vertici v
adiacenti a u, ovvero quei vertici v tali per cui esiste
un arco (u,v)E
• in particolare questo insieme di vertici è memorizzato
come una lista
• l’ordine dei vertici nella lista è arbitrario
Visualizzazione:
grafo non orientato
con liste di adiacenza
1
2
3
5
4
1
2
3
4
5
2
1
2
2
4
5
5
4
5
1
3
3
2
4
Visualizzazione:
grafo orientato
con liste di adiacenza
1
2
3
4
5
6
1
2
3
4
5
6
2
5
6
2
4
6
4
5
Proprietà della rappresentazione con liste
di adiacenza
• Se un grafo è orientato allora la somma delle
lunghezze di tutte le liste di adiacenza è |E|
– infatti per ogni arco (u,v) c’è un vertice v nella lista di
posizione u
• Se un grafo non è orientato allora la somma delle
lunghezze di tutte le liste di adiacenza è 2|E|
– infatti per ogni arco (u,v) c’è un vertice v nella lista di
posizione u e un vertice u nella lista di posizione v
• La quantità di memoria necessaria per memorizzare
un grafo (orientato o non) è O(max(V,E)) = O(V+E)
Grafi pesati
• In alcuni problemi si vuole poter associare una
informazione (chiamata peso) ad ogni arco
• un grafo con archi con peso si dice grafo pesato
• si dice che esiste una funzione peso che associa ad
un arco un valore
w:ER
• ovvero un arco (u,v) ha peso w(u,v)
Grafi pesati con liste di adiacenza
• Si memorizza il peso w(u,v) insieme al vertice v nella
lista per il vertice u
Visualizzazione:
grafo orientato pesato
con liste di adiacenza
1
0.2
2
0.1
0.4
0.3
3
0.6
4
0.5
1
2
3
4
5
6
0.2
5
6
0.8
2
5
6
2
4
6
0.2
0.4
0.2
0.1
0.5
0.8
4 0.3
5 0.6
Svantaggi della rappresentazione con
liste di adiacenza
• Per sapere se un arco (u,v) è presente nel grafo si
deve scandire la lista degli archi di u
Matrici di adiacenza
• Per la rappresentazione con matrici di adiacenza si
assume che i vertici siano numerati in sequenza da 1
a |V|
• Si rappresenta un grafo G=(V,E) con una matrice
A=(aij) di dimensione |V|x|V| tale che:
aij=1 se (i,j)  E
aij=0 altrimenti
Visualizzazione:
grafo non orientato
con matrice di adiacenza
1
2
3
5
4
1
2
3
4
5
1
0
1
0
0
1
2
1
0
1
1
1
3
0
1
0
1
0
4
0
1
1
0
1
5
1
1
0
1
0
Visualizzazione:
grafo orientato
1
2
3
4
5
6
1
2
3
4
5
6
1
0
0
0
0
0
0
2
1
0
0
1
0
0
3
0
0
0
0
0
0
4
1
0
0
0
1
0
5
0
1
1
0
0
0
6
0
0
1
0
0
1
Proprietà della rappresentazione con
matrice di adiacenza
• La rappresentazione di un grafo G=(V,E) con matrice
di adiacenza richiede memoria (V2)
indipendentemente dal numero di archi
• La matrice di adiacenza di un grafo non orientato è
simmetrica ovvero aij= aji
• Per un grafo non orientato si può allora memorizzare
solo i dati sopra la diagonale (diagonale inclusa),
riducendo della metà lo spazio per memorizzare la
matrice
Grafi pesati con matrici di adiacenza
• Si memorizza il peso nell’elemento aij invece di 1
• se l’arco non esiste si indica con 0 o  o NIL a
secondo del problema
Visualizzazione:
grafo orientato pesato
con matrice di adiacenza
1
0.2
2
0.1
0.4
0.3
3
0.6
4
0.5
1
2
3
4
5
6
0.2
5
6
0.8
1 2
0 .2
0 0
0 0
0 .1
0 0
0 0
3 4 5 6
0 .3 0 0
0 0 .4 0
0 0 .6 .2
0 0 0 0
0 .5 0 0
0 0 0 .8
Vantaggi della rappresentazione con
matrice di adiacenza
• la rappresentazione con matrice di adiacenza è
semplice
• se il grafo è piccolo non vi è sostanziale differenza di
efficienza con la rappresentazione con liste di
adiacenza
• per grafi non pesati si può rappresentare ogni singolo
elemento della matrice non con una parola ma con
un singolo bit
Visita in ampiezza
• La visita in ampiezza (breadth-first-search BFS) di un
grafo dato un vertice sorgente s consiste nella
esplorazione sistematica di tutti i vertici raggiungibili
da s in modo tale da esplorare tutti i vertici che hanno
distanza k prima di iniziare a scoprire quelli che
hanno distanza k+1
• inoltre la procedura di visita in ampiezza che
vedremo:
– calcola la distanza da s ad ognuno dei vertici raggiungibili
– produce un albero BFS che ha s come radice e che
comprende tutti i vertici raggiungibili da s
Idea intuitiva
• L’idea è quella di tenere traccia dello stato (già
scoperto, appena scoperto, ancora da scoprire) di
ogni vertice, “colorandolo” di un colore diverso
• i colori possibili sono:
– bianco: vertice ancora non scoperto
– grigio: vertice appena scoperto ed appartenente alla
frontiera
– nero: vertice per cui si è terminata la visita
• un vertice da bianco diventa grigio e poi nero
• se (u,v)  E ed u è un vertice nero, allora il vertice v
è grigio, ovvero tutti i vertici adiacenti ad un vertice
nero sono già stati scoperti
Visualizzazione
Idea intuitiva
• La visita in ampiezza costruisce un albero BFT
• la radice è il nodo sorgente s
• quando un vertice bianco v viene scoperto durante la
scansione della lista di adiacenza di un vertice già
scoperto u allora si aggiunge all’albero il vertice v e
l’arco (u,v)
• si dice che u è padre di v
• poiché un vertice viene scoperto al massimo una
volta ha al massimo un padre
Strutture ausiliarie
• La procedura di visita in ampiezza assume che il
grafo G=(V,E) sia rappresentato usando liste di
adiacenza
• ad ogni vertice u sono associati inoltre l’attributo
– colore: color[u]
– padre: [u]
– la distanza dalla sorgente s: d[u]
• L’algoritmo fa anche uso di una coda Q per gestire
l’insieme dei vertici grigi
Pseudocodice
BFS(G,s)
1 for ogni vertice u  V[G]-{s}
2 do
color[u]  WHITE
3
d[u]  
4
[u]  NIL
5 color[s]  GRAY
6 d[s]  0
7 [s]  NIL
8 Q  {s}
9 while Q  
10 do
u  head[Q]
11
for ogni v  Adj[u]
12
do
if color[v]=WHITE
13
then
color[v]  GRAY
14
d[v]  d[u]+1
15
[v]  u
16
Enqueue(Q,v)
17
Dequeue(Q)
18
color[u]  BLACK
Spiegazione del codice
• Le linee 1-4 eseguono l’inizializzazione:
– tutti i vertici sono colorati di bianco
– la distanza di tutti i vertici è non nota e posta a 
– il padre di ogni vertice inizializzato a nil
• la linea 5 inizializza la sorgente a cui:
– viene assegnato il colore grigio
– viene assegnata distanza 0
– viene assegnato padre nullo nil
• la linea 8 inizializza la coda Q con il vertice sorgente s
Spiegazione del codice
• Il ciclo principale è contenuto nelle linee 9-18
• il ciclo continua fino a quando vi sono vertici grigi in
Q, ovvero vertici già scoperti le cui liste di adiacenza
non siano state ancora completamente esaminate
• la linea 10 preleva l’elemento in testa alla coda
• nelle linee 11-16 si esaminano tutti i vertici v
adiacenti a u
• se v non è ancora stato scoperto lo si scopre
–
–
–
–
si colora di grigio
si aggiorna la sua distanza alla distanza di u +1
si memorizza u come suo predecesore
si pone in fondo alla coda
• quando tutti i vertici adiacenti a u sono stati scoperti
allora si colora u di nero e lo si rimuove da Q
Visualizzazione
r
s
t
u
0
r
s
1
0
t
u
Q:s
Q:wr
1
v
w
x
y
v
w
x
y
r
s
t
u
r
s
t
u
1
0
2
1
0
2
Q:rtx
v
1
2
w
x
y
Q:txv
2
1
2
v
w
x
y
Visualizzazione
r
s
t
u
r
s
t
u
1
0
2
3
1
0
2
3
Q:xvu
2
1
2
v
w
x
r
s
1
0
Q:vuy
2
1
2
3
y
v
w
x
y
t
u
r
s
t
u
2
3
1
0
2
3
Q:uy
Q:y
2
1
2
3
2
1
2
3
v
w
x
y
v
w
x
y
Analisi
• Il tempo per l’inizializzazione è O(V)
• Dopo l’inizializzazione nessun vertice sarà mai
colorato più di bianco
• quindi il test in 12 assicura che ogni vertice sarà
inserito nella coda Q al più una volta
• le operazioni di inserimento ed eliminazione dalla
coda richiedono un tempo O(1)
• il tempo dedicato alla coda nel ciclo 9-18 sarà
pertanto un O(V)
Analisi
• poiché la lista di adiacenza è scandita solo quando si
estrae il vertice dalla coda allora la si scandisce solo
1 volta per vertice
• poiché il numero di archi è pari a |E| allora la somma
delle lunghezze di tutte le liste è (E)
• allora il tempo speso per la scansione delle liste
complessivamente è O(E)
• in totale si ha un tempo di O(V+E)
• quindi la procedura di visita in ampiezza richiede un
tempo lineare nella rappresentazione con liste di
adiacenza
Alberi BFS
• La procedura BFS costruisce un albero BFS durante
la visita del grafo
• l’informazione sull’albero è contenuta nei puntatori al
padre 
• formalmente, dato G=(V,E) con sorgente s si
definisce il sottografo dei predecessori di G come
G=(V,E) dove:
V ={v  V : [v]  NIL}  {s}
E={([v],v)  E : v  V -{s}}
Alberi BFS
• G è un albero BFS se
– V contiene tutti e soli i vertici raggiungibili da s
– e se per ogni v  V vi è un unico cammino semplice da s a
v in G che è anche un cammino minimo da s a v in G.
• Un albero BFS è effettivamente un albero perché è
connesso e |E|=|V|-1
• si dimostra che dopo aver eseguito la procedura BFS
a partire da una sorgente s, il sottografo dei
predecessori è effettivamente un albero BFS
Visualizzazione dell’albero BFS
r
s
t
u
1
0
2
3
s
0
r
2
1
2
3
v
w
x
y
1
2
1
w
2
x
2
t
v
3
y
3
u
Visita in profondità
• La visita in profondità (depth-first-search DFS) di un
grafo consiste nella esplorazione sistematica di tutti i
vertici andando in ogni istante il più possibile in
profondità
• gli archi vengono esplorati a partire dall’ultimo vertice
scoperto v che abbia ancora archi non esplorati
uscenti
• quando questi sono finiti si torna indietro per
esplorare gli altri archi uscenti dal vertice dal quale v
era stato scoperto
Visita in profondità
• Il procedimento continua fino a quando non vengono
scoperti tutti i vertici raggiungibili dal vertice sorgente
originario
• se al termine rimane qualche vertice non scoperto
uno di questi diventa una nuova sorgente e si ripete
la ricerca a partire da esso
• questo fino a scoprire tutti i vertici
Visita in profondità
• A differenza che nella visita per ampiezza il cui
sottografo dei predecessori formava un albero, nel
caso della visita in profondità si forma una foresta di
diversi alberi DFS
• infatti si hanno più sorgenti (radici)
Visualizzazione
Idea intuitiva
• Come per la visita in ampiezza i vertici vengono
colorati per tenere conto dello stato di visita:
– ogni vertice è inizialmente bianco
– è grigio quando viene scoperto
– viene reso nero quando la visita è finita, cioè quando la sua
lista di adiacenza è stata completamente esaminata
Marcatura temporale
• Oltre al colore si associa ad ogni vertice v due
informazioni temporali:
– tempo di inizio visita d[v], cioè quando è reso grigio per la
prima volta
– tempo di fine visita f[v], cioè quando è reso nero
• il valore temporale è dato dall’ordine assoluto con cui
si colorano i vari vertici del grafo
• si usa per questo una variabile globale tempo che
viene incrementata di uno ogni volta che si esegue
un inizio di visita o una fine visita
Marcatura temporale
• il tempo è un intero compreso fra 1 e 2|V| poiché ogni
vertice può essere scoperto una sola volta e la sua
visita può finire una sola volta
• per ogni vertice u si ha sempre che d[u]<f[u]
• ogni vertice u è
– WHITE prima di d[u]
– GRAY fra d[u] e f[u]
– BLACK dopo f[u]
Utilità della marcatura temporale
• La marcatura temporale è usata in molti algoritmi sui
grafi
• E’ utile in generale per ragionare sul comportamento
della visita in profondità
Pseudocodice
DFS(G)
1 for ogni vertice u  V[G]
2 do
color[u]  WHITE
3
[u]  NIL
4 time  0
5 for ogni vertice u  V[G]
6
do
if color[u]=WHITE
7
then
DFS-Visit(u)
DFS-Visit(u)
1 color[u]  GRAY
2 d[u]  time  time +1
3 for ogni v  Adj[u]
4 do
if color[v]=WHITE
5
then
[v]  u
6
DFS-Visit(v)
7 color[u]  BLACK
8 f[u]  time  time +1
Spiegazione dello pseudocodice
• Le righe 1-4 della procedura DFS eseguono la fase di
inizializzazione colorando ogni vertice del grafo di
bianco, settando il padre a NIL e impostandola
variabile globale time a 0
• il ciclo 5-7 esegue la procedura DFS-Visit su ogni
nodo non ancora scoperto del grafo, creando un
albero DFS ogni volta che viene invocata la
procedura
Spiegazione dello pseudocodice
• In ogni chiamata DFS-Visit(u) il vertice u è
inizialmente bianco
• viene reso grigio e viene marcato il suo tempo di
inizio visita in d[u], dopo aver incrementato il
contatore temporale globale time
• vengono poi esaminati tutti gli archi uscenti da u e
viene invocata ricorsivamente la procedura nel caso
in cui i vertici collegati non siano ancora stati
esplorati
• in questo caso il loro padre viene inizializzato ad u
• dopo aver visitato tutti gli archi uscenti u viene
colorato BLACK e viene registrato il tempo di fine
visita in f[u]
Visualizzazione
u
v
w
1/
u
v
1/
2/
w
u
v
1/
2/
w
3/
x
y
u
v
1/
2/
4/
3/
x
y
z
w
z
x
y
u
v
1/
2/
4/5
3/
x
y
z
x
y
z
w
u
v
w
1/
2/
4/5
3/6
x
y
z
z
Visualizzazione
u
v
1/
2/7
4/5
3/6
x
y
u
1/8
w
u
1/8
v
w
u
1/8
2/7
4/5
3/6
z
x
y
z
v
w
u
v
w
u
2/7
9/
1/8
2/7
9/
1/8
v
w
2/7
9/
4/5
3/6
x
y
v
2/7
z
w
9/12
4/5
3/6
10/
4/5
3/6
10/11
4/5
3/6
10/11
x
y
z
x
y
z
x
y
z
Analisi del tempo di calcolo
• Il ciclo di inizializzazione di DFS e il ciclo for 5-7
richiedono entrambi tempo (V)
• la procedura DFS-Visit viene chiamata solo una volta
per ogni vertice (poiché viene chiamata quando il
vertice è bianco e lo colora immediatamente di grigio)
• in DFS-Visit il ciclo for 3-6 viene eseguito |Adj[v]|
volte, e dato che la somma della lunghezza di tutte le
liste di adiacenza è (E), il costo è (E)
• il tempo totale è quindi un (V+E)
Ordinamento topologico
• L’ordinamento topologico è un ordinamento definito
su i vertici di un grafo orientato aciclico (directed
acyclic graph DAG)
• si può pensare all’ordinamento topologico come ad
un modo per ordinare i vertici di un DAG lungo una
linea orizzontale in modo che tutti gli archi orientati
vadano da sinistra verso destra
Ordinamento topologico
• I grafi aciclici diretti sono utilizzati per modellare precedenze fra
eventi
• consideriamo ad esempio le precedenze nelle operazioni del
vestirsi utilizzando un DAG i cui nodi siano indumenti
• certi indumenti vanno messi prima di altri (i calzini prima delle
scarpe)
• mentre altri indumenti possono essere indossati in qualsiasi
ordine (calzini e pantaloni)
• un arco orientato (u,v) indica che l’indumento u deve essere
indossato prima dell’indumento v
• l’ordinamento topologico del DAG fornirà dunque un ordine per
vestirsi
Visualizzazione
calzini
11/14
15/16
pantaloni
orologio
scarpe
12/13
6/7
camicia
1/8
cravatta
2/5
giacca
3/4
cintura
9/10
Visualizzazione
calzini
15/16
pantaloni
scarpe
orologio
camicia
cintura
11/14
12/13
9/10
1/8
6/7
cravatta
2/5
giacca
3/4
Pseudocodice
Topological-Sort(G)
1 chiama DFS(G) per calcolare f[v] per ogni v
2 appena la visita di un vertice è finita inseriscilo in
testa ad una lista
3 return la lista concatenata dei vertici
Analisi
• Si esegue un ordinamento topologico in tempo
O(V+E) dato che:
– la visita DFS richiede un tempo O(V+E)
– l’inserimento di ognuno dei |V| vertici richiede ciascuno un
tempo O(1)