




















Studia grazie alle numerose risorse presenti su Docsity
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Prepara i tuoi esami
Studia grazie alle numerose risorse presenti su Docsity
Prepara i tuoi esami con i documenti condivisi da studenti come te su Docsity
Trova i documenti specifici per gli esami della tua università
Preparati con lezioni e prove svolte basate sui programmi universitari!
Rispondi a reali domande d’esame e scopri la tua preparazione
Riassumi i tuoi documenti, fagli domande, convertili in quiz e mappe concettuali
Studia con prove svolte, tesine e consigli utili
Togliti ogni dubbio leggendo le risposte alle domande fatte da altri studenti come te
Esplora i documenti più scaricati per gli argomenti di studio più popolari
Ottieni i punti per scaricare
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
si tratta del corso di algoritmo
Tipologia: Appunti
1 / 28
Questa pagina non è visibile nell’anteprima
Non perderti parti importanti!





















Siano a≥1 e b>1 costanti e f(n) una funzione, e T(n) sia definito sugli interi non negativi dalla ricorrenza:
T(n) = aT(n/b) + f(n) ;
allora T(n) può essere asintoticamente limitato come segue:
1. Se f(n) = O(𝑛
𝑙𝑜𝑔
𝑏
𝑎−𝜀
) per qualche costante ε>0, allora T(n) = Ɵ(𝑛
𝑙𝑜𝑔
𝑏
𝑎
2. Se f(n) = Ɵ(𝑛
𝑙𝑜𝑔
𝑏
𝑎
), allora T(n) = Ɵ(𝑛
𝑙𝑜𝑔
𝑏
𝑎
3. Se f(n) = Ω(𝑛
𝑙𝑜𝑔
𝑏
𝑎+𝜀
), per qualche costante ε>0, e se a.f(n/b) 0 e A[i] > key
L’indice j indica la “carta corrente” che deve essere inserita nella mano, gli elementi A[1…j-1] dell’array
costituiscono le carte già ordinate che sono nella mano ed infine, gli elementi A[j+1…n] corrispondono al
mazzetto di carte ancora sul tavolo.
L’indice j si sposta da sinistra a destra su tutto l’array: ad ogni iterazione del ciclo for “più esterno”,
l’elemento A[j] è copiato fuori dall’array (linea 2), quindi cominciando dalla posizione j-1, gli elementi sono
spostati ad uno ad uno di una posizione a destra finché non viene trovata la giusta posizione per A[j] (linne
4 - 7): a questo punto l’elemento A[j] è inserito (linea 8).
Complessità di Insertion Sort
Per ciascun j=2,3,…,n, dove n=length[A], sia t j
il numero di volte che il test del ciclo while alla linea 5 è
eseguito per quel valore j.
INSERTION SORT (A) Costo N° di volte
n
n- 1
n- 1
𝑗
𝑛
𝑗= 2
𝑗
𝑛
𝑗= 2
𝑗
𝑛
𝑗= 2
n- 1
Dunque:
T(n) = c 1
n + c 2
(n-1) + c 4
(n-1) + c 5
𝑗
𝑛
𝑗= 2
𝑗
𝑛
𝑗= 2
𝑗
𝑛
𝑗= 2
(n-1) ;
j=1,2,…,n, si ha che A[i] ≤ key nella linea 5, quando i ha il suo valore iniziale di j - 1.
Quindi t j
= 1 per ogni j= 2,3,…,n, con un tempo di esecuzione pari a:
T(n) = c 1
n + c 2
(n-1) + c 4
(n-1) + c 5
(n-1) + c 8
(n-1) = (c 1
c 2
c 4
c 5
c 8
)n – (c 2
c 4
c 5
c 8
Dunque si ha T(n)=Ɵ(n).
bisogna confrontare ogni elemento A[j] con ogni elemento nell’intero sottoarray A[1…j-1], così che
t j
= j per j = 2,3,…,n.
Sfruttando che: ∑ 𝑗
𝑛
𝑗= 2
𝑛
( 𝑛+ 1
)
2
− 1 , e che ∑ (𝑗 − 1 )
𝑛
𝑗= 2
𝑛
( 𝑛− 1
)
2
si trova che nel caso peggiore il tempo di esecuzione di insertion sort è:
T(n) = c 1
n + c 2
(n-1) + c 4
(n-1) + c 5
𝑛
( 𝑛+ 1
)
2
− 1 ) + c 6
𝑛
( 𝑛+ 1
)
2
− 1 ) + c 7
𝑛
( 𝑛+ 1
)
2
− 1 ) + c 8
(n-1) =
𝑐 5
2
𝑐 6
2
𝑐 7
2
) n
2
1
2
4
𝑐 5
2
𝑐 6
2
𝑐 7
2
8
) n – (𝑐
2
4
5
8
Dunque si ha T(n) = Ɵ(n
2
aver scelto n numeri a caso a cui applicare l’insertion sort; quanto tempmo si impiega per
determinare dove inserire A[j] nel sottoarray A[1…j-1]? Mediamente metà elementi di A[1…j-1]
sono più piccoli di A[j] e quelli dell’altra metà sono più grandi.
In media perciò si deve verificare solo una metà di A[1…j-1] e quindi t j
= j/2. Calcolando il risultante
tempo di esecuzione nel caso medio, si ottiene una funzione quadratica della dimensione
dell’input, proprio come per il tempo di esecuzione nel caso peggiore.
Dunque T(n) = Ɵ(n
2
L’algoritmo merge sort segue in modo stretto la filosofia del divide-et-impera; intuitivamente esso funziona
nel seguente modo:
ciascuna;
L’operazione chiave dell’algoritmo di merge sort è la fusione delle due sottosequenze ordinate nel passo
“combina”. Per eseguire una tale fusione usa una procedura ausiliaria MERGE (A,p,q,r), dove A è un array,
p,q ed r sono indici di elementi dell’array tali che p≤q≤r; questa procedura, assumendo che A[p…q] e
A[q+1…r] siano ordinati, genera un singolo sottoarray che sostituisce il corrente sottoarray A[p…r].
Complessità di merge sort
Siccome è un algoritmo che richiama se stesso, il tempo di esecuzione di Merge sort può essere descritto
con una ricorrenza , che descrive il tempo di esecuzione complessivo di un problema di dimensione n in
termini del tempo di esecuzione per input più piccoli.
Si può ipotizzare, senza perdita di generalità, che il problema originale di Merge sort abbia una dimensione
che è una potenza di due. In tal caso, ciascun passo di divisione produce due sottosequenze di dimensione
n/2.
impiega un tempo costante; quando si hanno n>1 elementi, si suddivide il tempo di esecuzione
come illustrato:
o DIVIDE: il passo di divisione calcola l’indice di mezzo dell’array impiegando, quindi, un
tempo costante, da cui D(n)=Ɵ(n);
o IMPERA: si risolvono ricorsivamente due sottoproblemi, ciascuno di dimensione n/2, che
contribuiscono per 2T(n/2) al tempo di esecuzione;
o COMBINA: si è già esaminato il fatto che la procedura MERGE su sottoarray di n elementi
impiega un tempo di Ɵ(n), per cui C(n)= Ɵ(n).
Addizionando le funzioni D(n) e C(n), si sta sommando una funzione Ɵ(n) ed una funzione Ɵ(1),
ossia si ottiene una funzione lineare Ɵ(n); addizionandola al termine del passo “impera”, si ottiene
la ricorrenza T(n) per il tempo di esecuzione nel caso peggiore del merge sort:
T(n) = {
𝑛
2
) + Ɵ(n) 𝑠𝑒 𝑛 > 1
Applicando il teorema maestro, si ottiene che T(n) = Ɵ(n lgn)
L’algoritmo heap sort impiega l’uso di una struttura di dati denominato come “ heap ” (in italiano, cataste),
ossia un array che può essere visto come un albero binario quasi completo, in cui ogni nodo dell’albero
corrisponde ad un elemento dell’array che contiene il valore del nodo. L’albero è riempito completamente
su tutti i livelli tranne, eventualmente, il più basso che è riempito da sinistra in poi. Un array A che
rapresenta uno heap è un oggetto con due attributi: length[A], che è il numero di elementi dell’array, e
heap-size[A], il numero di elementi dello heap memorizzati nell’array A. La radice dell’albero è A[1], e se i è
l’indice di un nodo, l’indice del padre PARENT(i), del figlio sinistro LEFT(i) e del figlio destro RIGHT(i)
possono essere calcolati semplicemente:
PARENT (i)
Return parte_int_inf(i/2)
LEFT (i)
Return 2i
RIGHT (i)
Return 2i + 1
Gli heap soddisfano la proprietà dello heap , cioè per ogni nodo i diverso dalla radice, A[PARENT(i)]≥A[i],
ossia il valore di un nodo è minore o uguale al valore del padre. Quindi l’elemento più grande nello heap è
memorizzato nella radice ed i sottoalberi di qualunque nodo contengono valori non maggiori del valore del
nodo stesso.
L’ altezza di un nodo di un albero è il numero di archi sul più lungo cammino semplice discendente che va
dal nodo ad una foglia, e si definisce altezza dell’albero l’altezza della sua radice. Poiché uno heap di n
elementi si basa su un albero binario completo, osservando che
ℎ
ℎ+ 1
2
2
𝑛 , segue che h = Ɵ(lg n).
Per mantenere la proprietà dello heap si utilizza la procedura HEAPIFY. I suoi input sono un array A ed un
indice i nell’array. Quando HEAPIFY viene chiamata, si assume che gli alberi binari con radice in LEFT(i) e
RIGHT(i) siano heap, ma che A[i] possa essere più piccolo dei suoi figli, violando così la proprietà dello heap.
La funzione di HEAPIFY è di lasciar “scendere” il valore di A[i] nello heap in modo che il sottoalbero con
radice di indice i diventi uno heap.
HEAPIFY (A,i)
Ad ogni passo, è determinato il più grande tra A[i],A[LEFT(i)] e A[RIGHT(i)] ed il suo indice è memorizzato in
largest. Se A[i] è più grande, allora il sottoalbero con radice nel nodo i è uno heap e la procedura termina.
Complessità di heapsort:
La procedura HEAPSORT impiega un tempo O=(n lgn), poiché la chiamata a BUILD-HEAP impiega un tempo
O(n) e ognuna delle n-1 chiamate a HEAPIFY impiega un tempo O(lgn).
Il quicksort è un algoritmo di ordinamento che spesso risulta la migliore scelta pratica di ordinamento
perché in media è notevolmente efficiente: il suo tempo medio di esecuzione è Ɵ(n lgn), ed i fattori costanti
nasacosti dalla notazione Ɵ(n lgn) sono sufficientemente piccoli. Offre inoltre il vantaggio di ordinare in
loco.
Quicksort utilizza un sottoprogramma usato per il partizionamento chiamato partition , che risistema il
sottoarray A[p…r] in loco.
Il quicksort, come il merge-sort, è basato sulla filosofia divide-et-impera, che richiede le seguenti 3 fasi:
ogni elemento di A[p…q] sia minore o uguale a qualunque elemento di A[pq+1…r]. L’indice q è
calcolato dalla procedura di partizionamento PARTITION;
ricombinarli: l’intero array A[p…r] è subito ordinato. La seguente procedura realizza il quicksort:
QUICKSORT (A,p,r)
PARTITION (A,p,r)
Concettualmente, la procedura di partizione esegue una funzione semplice: pone gli elementi più piccoli di
x nella regione bassa dell’array e quelli più grandi di x nella regione alta.
Il tempo di esecuzione di PARTITION su un array A[p…r] è Ɵ(n), dove n=r-p+1.
Complessità di quicksort
Il tempo di esecuzione di quicksort dipende dal fatto che il partizionamento sia bilanciato o sbilanciato e ciò
a sua volta dipende da quali elementi sono usati per il partizionamento. Se il partizionamento è bilanciato,
l’algoritmo viene eseguito con la stessa velocità asintotica del merge-sort. Se il partizionamento è
sbilanciato tuttavia può essere asintoticamente lento come l’insertion sort.
Il comportamento peggiore del quicksort avviene quando il sottoprogramma di partizionamento
produce una regione con n-1 elementi ed una con un solo elemento.
Supponendo che questo partizionamento sbilanciato avvenga ad ogni passo dell’algoritmo, poiché
il partizionamento richiede tempo Ɵ(n) e T(1)= Ɵ(1), la ricorrenza per il tempo di esecuzione è:
T(n)=T(n-1) + Ɵ(n)
Osservando che T(1)= Ɵ(1), si ha:
T(n)=T(n-1) + Ɵ(n) =
k
𝑛
𝑘= 1
k
𝑛
𝑘= 1
) = Ɵ(n
2
dove l’ultima uguaglianza si ottiene osservando che ∑ k
𝑛
𝑘= 1
è la serie aritmetica.
Nell’analizzare il comportamento di quicksort nel caso medio, è stata fatta l’ipotesi che tutte le
permutazioni dei numeri in input fossero equivalentemente probabili. Tuttavia quest’ipotesi non vale
sempre.
Si passa dunque alla versione Randomized quicksort , il cui comportamento non dipende solo dall’input, ma
anche da valori prodotti da un generatore di numeri pseudo-casuali. Questa versione di Quicksort ha la
proprietà che nessun input particolare provoca il caso peggiore nel comportamento dell’algoritmo. Il caso
peggiore dipende invece dal generatore di numeri casuali: anche intenzionalmente, non si riesce a produrre
un cattivo array di input, in quanto le permutazioni casuali rendono irrilevante l’ordine dell’input.
L’algoritmo randomizzato si comporta male solo se il generatore di numeri casuali produce una
permutazione sfortunata da ordinare.
Modificando la procedura PARTITION, si può progettare un’altra versione randomizzata del quicksort che
usi la seguente strategia di scelta casuale: ad ogni passo dell’algoritmo quicksort, prima di partizionare
l’array, si scambia l’elemento A[p] con un elemento a caso in A[p…r]. Questa modifica assicura che
l’elemento perno x=A[p] sia in modo equiprobabile uno qualunque degli r-p+1 elementi del sottoarray. Di
conseguenza ci si aspetta che la suddivisione dell’array in input sia, in media, ragionevolmente ben
bilanciata. La nuova procedura di partizionamento è:
RANDOMIZED-PARTITION (A,p,r)
Il nuovo quicksort si chiama adesso RANDOMIZED-QUICKSORT:
RANDOMIZED-QUICKSORT (A,p,r)
Il counting-sort si basa sull’ipotesi che ognuno degli n elementi di input sia un intero nell’intervallo da 1 a k,
per un certo intero k. Quando k=O(n), l’ordinamento viene eseguito in tempo O(n).
L’idea di base di counting-sort è di determinare, per ogni elemento x in input, il numero di elementi minori
di x. Questa informazione può essere usata per porre x direttamente nella sua posizione nell’array di
output. Per esempio, se vi sono 17 elementi minori di x, allora x v messo in posizione 18 dell’output.
Nel codice di counting-sort si assume che l’input sia un array A[1…n] e che length[A]=n.
Inoltre, sono richiesti altri due array: l’array B[1…n] mantiene l’output ordinato e l’array C[1…k] fornisce la
memoria di lavoro temporanea.
COUNTING-SORT (A,B,k)
Complessità di counting-sort
Il ciclo for nelle linee 1-2 impiega un tempo O(k), il ciclo for nelle linee 3-4 impiega un tempo O(n), il ciclo
for nelle linee 6 - 7 impiega un tempo O(k) ed il ciclo for nelle linee 9 - 11 impiega un tempo O(n). Dunque il
tempo totale è O(n+k); tuttavia nelle ipotesi di counting-sort si era supposto che k=O(n), da cui la
complessità complessiva è O(n).
(elementi) ed n contenitori (bucket) ed ogni pallina è lanciata indipendentemente con probabilità p=1/n di
cadere in qualunque bucket. Così la probabilità che n i
= k segue che la distribuzione binomiale b(k;n,p), che
ha valore medio E[n i
] = np = 1 e varianza Var[n i
]=np(1-p)=1-1/n. Per qualunque variabile casuale X,
l’equazione precedente fornisce:
E[n
i
2
] = Var[n i
2
[n i
] = 1 - 1/n + 1
2
= 2- 1/n = Ɵ(1).
Utilizzando questo limite, si conclude che il tempo atteso nel caso medio per l’ordinamento con l’insertion
sort è O(n).
Un grafo orientato G è una coppia (V,E) dove V è l’ insieme dei vertici mentre E è l’ insieme degli archi.
Un grafo non orientato G è una coppia (V,E) dove V è l’ insieme dei vertici ed E è costituito da coppie di
vertici non ordinate : l’arco (u,v) e l’arco (v,u) sono considerati lo stesso arco.
vertice u) ed è incidente il vertice v (o che entra nel vertice v);
il grado entrante è il numero di archi che entrano in esso. Il grado di un vertice è la somma dei
gradi entranti ed uscenti;
Un cammino di lunghezza k da un vertice u ad un vertice u’ di un grafo G è una sequenza di
vertici tale che v 0 =u, vk=u’, e l’arco (vi- 1 ,vi) appartiene ad E per ogni i=1,2,…,k. La lunghezza di un cammino è
il numero di archi di un cammino. Si dice che il cammino contiene i vertici v 0
,v 1
,v 2
,…,v k
e gli archi
(v 0
,v 1
),(v 1
,v 2
),…,(v k- 1
,v k
). Se esiste un cammino p da un vertice u ad un vertice u’, si dice che u’ è
raggiungibile da u tramite il cammino p; si denoterà con u u’.
Un cammino è semplice se tutti i vertici contenuti sono distinti.
Un sottocammino di un cammino p= è una sottosequenza contigua dei suoi vertici <
v i
,v i+
,…,v j
> per ogni 1≤i≤j≤k.
,v 1
,v 2
,…,v k
> forma un ciclo se v 0
=v k
ed il cammino contiene
almeno un arco.
Un ciclo è semplice se v 1
,v 2
,…,v k
sono tutti distinti. Un cappio è un ciclo di lunghezza 1.
Un grafo orientato senza cappi si dice grafo semplice.
,v 1
,v 2
,…,v k
> forma un ciclo (semplice) se k≥3, v 0
= v k
e
v 1
,v 2
,…,v k
sono tutti distinti.
Un grafo senza cicli si dice grafo aciclico.
Vi sono due modi standard di rappresentare un grafo G=(V,E): come una collezione di liste di adiacenza o
come una matrice di adiacenza. Di solito si preferisce la rappresentazione con liste di adiacenza perché
essa fornisce un modo compatto di rappresentare grafi sparsi , ossia quelli per cui |E| è molto minore di
2
. Tuttavia una rappresentazione con matrice di adiacenza può essere preferita quando il grafo è denso ,
ossia quando |E| è vicino a |V|
2
, oppure quando occorre essere in grado di dire rapidamente se vi è un
arco che collega due vertici dati.
La rappresentazione con liste di adiacenza di un grafo G=(V,E) consiste in un vettore Adj di |V| liste, una
per ogni vertice in V. Per ogni vertice u, la lista di adiacenza Adj[u] contiene (puntatori a) tutti i vertici v tali
che esista un arco (u,v) appartenente ad E; quindi Adj[u] comprende tutti i vertici adiacenti ad u in G. In
ogni lista di adiacenza i vertici vengono di solito memorizzati in un ordine arbitrario.
Se G è un grafo orientato, la somma delle lunghezze di tutte le liste di adiacenza è |E|, perché un arco della
forma (u,v) è rappresentato ponendo v in Adj[u]. Se G è un grafo non orientato, la somma delle lunghezze
di tutte le liste di adiacenza è 2|E|, perché se (u,v) è un arco non orientato, allora u appare nella lista di
adiacenza di v e viceversa. Sia per un grafo orientato sia per uno non orientato la rappresentazione con liste
di adiacenza gode della desiderabile proprietà che la quantità di memoria necessaria è O(max(V,E)) =
Le liste di adiacenza possono essere facilmente adattate per rappresentare grafi pesati , cioè grafi per i quali
ogni arco ha associato un peso normalmente dato da una funzione peso w:E-> R. Ad esempio, sia G=(V,E)
un grafo pesato con una funzione peso w. Il peso w(u,v) dell’arco (u,v) viene memorizzato semplicemente
insieme al vertice v nella lista di adiacenza di u. La rappresentazione con liste di adiacenza è abbastanza
robusta, nel senso che può essere adattata a molte altre varianti di grafi.
Uno svantaggio potenziale della rappresentazione con liste di adiacenza è che per determinare se un dato
arco (u,v) è presente nel grafo non vi è metodo più veloce che cercare v nella lista di adiacenza Adj[u]. Si
può porre rimedio a questo svantaggio con una rappresentazione del grafo come matrice di adiacenza, al
costo di usare asintoticamente più memoria.
Per la rappresentazione con matrice di adiacenza di un grafo G=(V,E), si assume che i vertici siano numerati
1,2,…,|V| in modo arbitrario. La rappresentazione con matrice di adiacenza di un grafo G consiste in una
matrice A=(a ij
) di dimensione |V|x|V| tale che:
a ij
Come per la rappresentazione con liste di adiacenza, anche la rappresentazione con matrice di adiacenza
può essere usata per grafi pesati. Ad esempio, se G=(V,E) è un grafo pesato con funzione peso w, il peso
w(u,v) dell’arco (u,v)ϵE viene memorizzato semplicemente come l’elemento in riga u e colonna v della
matrice di adiacenza. Se un arco non esiste si può memorizzare un valore NIL nella corrispondente
posizione della matrice, anche se per molti problemi può essere conveniente usare in valore come 0 oppure
Sebbene la rappresentazione con liste di adiacenza sia asintoticamente almeno tanto efficiente quanto la
rappresentazione con matrici di adiacenza, la semplicità di una matrice di adiacenza può renderla
preferibile quando i grafi siano ragionevolmente piccoli. Inoltre, se il grafo non è pesato, vi è un ulteriore
vantaggio per la rappresentazione con matrice di adiacenza riguardante la memorizzazione: infatti, invece
di usare una intera parola di memoria per ogni elemento della matrice di adiacenza, si può, usare un
singolo bit.
BFS (G,s)
La procedura di visita in ampiezza che precede, chiamata BFS, assume che il grafo in input G=(V,E) sia
rappresentato usando liste di adiacenza. Essa mantiene diverse strutture di dati addizionali associate ad
ogni vertice del grafo: ad esempio, il colore di ogni vertice uϵV è memorizzato nella variabile color[u],
mentre il predecessore di u viene ricordato nella variabile π[u]. Se u non ha predecessore (ad esempio se
u=s o se u non è stato scoperto), allora π[u]=NIL. La distanza dalla sorgente s al vertice u calcolata
dall’algoritmo viene memorizzata in d[u].
La procedura BFS funziona nel modo seguente. Le linee 1-4 colorano tutti i vertici di bianco, assegnano
infinito alla variabile d[u] per ogni vertice u ed inizializzano il predecessore di ogni vertice con NIL. La linea 5
colora il vertice sorgente s di grigio, poiché si assume che esso venga scoperto non appena parte la
procedura; la linea 6 inizializza d[s] con 0 e la linea 7 pone NIL come predecessore della sorgente. La linea 8
inizializza Q con la coda che contiene il solo vertice s; da questo momento in poi Q conterrà sempre
l’insieme dei vertici grigi.
Il ciclo principale del programma è contenuto nelle linee 9-18. Il ciclo viene ripetuto finché esistono dei
vertici grigi, cioè dei vertici già scoperti le cui liste di adiacenza non siano ancora state completamente
esaminate. La linea 10 determina il vertice grigio u che si trova in testa alla coda Q. Il ciclo for nelle linee 11-
16 esamina ogni vertice v nella lista di adiacenza di u. Se v è bianco (e quindi non è stato ancora scoperto)
l’algoritmo lo “scopre” eseguendo le linee 13-16: dapprima esso viene colorato di grigio e la sua distanza
d[v] viene posta a d[u]+1; quindi u viene memorizzato come suo predecessore; infine v viene posto in fondo
alla coda Q. Quando tutti i vertici della lista di adiacenza di u sono stati esaminati, nelle linee 17-18 u viene
rimosso da Q e viene colorato di nero.
Complessità di BFS
Sia G=(V,E) il grafo in input. Dopo l’inizializzazione nessun vertice verrà mai più colorato di bianco, e quindi
il test della linea 12 assicura che ogni vertice venga inserito nella coda al massimo una volta, e di
conseguenza che venga eliminato dalla coda al massimo una volta. Le operazioni di inserimento e di
eliminazione dalla coda richiedono tempo O(1), quindi il tempo totale dedicato alle operazioni sulla coda è
O(V). Poiché la lista di adiacenza di ogni vertice viene scandita solo quando il vertice è estratto dalla coda, la
lista di adiacenza di ogni vertice viene scandita al massimo una volta; inoltre, poiché la somma delle
lunghezze di tutte le liste di adiacenza è Ɵ(E). Infine il tempo necessario per l’inizializzazione è O(V), e
quindi il tempo totale di esecuzione della procedura BFS è O(V+E): di conseguenza la visita in ampiezza
richiede un tempo lineare nella dimensione della rappresentazione con liste di adiacenza di G.
Cammini minimi
Si definisce la distanza sul cammino minimo δ(s,v) da s a v come il minimo numero di archi di un cammino
dal vertice s al vertice v, oppure ∞ se non esiste nessun cammino da s e v. Un cammino di lunghezza δ(s,v)
da s a v è chiamato un cammino minimo da s a v.
Lemma
Sia G=(V,E) un grafo orientato o non orientato, e sia s ϵ V un vertice arbitrario. Allora per ogni arco (u,v) ϵ E
si ha: δ(s,v) ≤ δ(s,u) + 1.
Lemma
Sia G=(V,E) un grafo orientato o non orientato, e si supponga che la procedura BFS venga eseguita su G a
partire da un dato vertice sorgente s ϵ V. Allora, al termine della procedura, per ogni vertice v ϵ V il valore
d[v] calcolato da BFS soddisfa d[v] ≥ δ(s,v).
La procedura DFS funziona nela seguente modo. Le linee 1-3 colorano tutti i vertici di bianco ed inizializzano
i loro campi π a NIL; la linea 4 azzera il contatore globale del tempo. Le linee 5-7 controllano tutti i vertici di
V, e quando ne trovano uno bianco lo visitano usando la procedura DFS-VISIT. Ogni volta che DFS-VISIT(u)
viene invocata nella linea 7, il vertice u diventa radice di un nuovo albero della foresta DFS. Quando DFS
termina, ad ogni vertice u è stato assegnato un tempo di scoperta d[u] ed un tempo di fine visita f[u].
In ogni chiamata DFS-VISIT(u), il vertice u è inizialmente bianco. La linea 1 colora u di grigio e la linea 2
memorizza il tempo di scoperta d[u] incrementando la variabile globale time e memorizzandone il valore.
Le linee 3-6 esaminano ogni vertice v adiacente ad u e visitano ricorsivamente v se esso è bianco. Non
appena un vertice v ϵ Adj[u] viene considerato nella linea 3, diciamo che l’arco (u,v) è stato esplorato dalla
visita in profondità. Infine, dopo che ogni arco uscente da u è stato esplorato, le linee 7-8 colorano u di
nero e registrano il tempo di fine visita f[u].
Complessità di DFS
I due cicli che si trovano rispettivamente nelle linee 1-3 e 5-7 di DFS richiedono tempo Ɵ(V), escluso il
tempo necessario per eseguire le chiamate a DFS-VISIT. La procedura DFS-VISIT è chiamata esattamente
una volta per ogni vertice v ϵ V, poiché DFS-VISIT viene invocata solo su vertici bianchi e la prima cosa che
fa è di colorare il vertice di grigio. Durante una esecuzione di DFS-VISIT(v) il ciclo nelle linee 3-6 viene
eseguito |Adj[v]| volte. Poiché
∑ |Adj[v]|
v ϵ V
il costo totale dell’esecuzione delle linee 3-6 di DFS-VISIT è Ɵ(E). Quindi il tempo di esecuzione di DFS è
Proprietà della visita in profondità
La visita in profondità fornisce molte informazioni sulla struttura di un grafo. Forse la più importante della
visita in profondità è che il sottografo dei predecessori G π
forma effettivamente una foresta di alberi,
poiché la struttura degli alberi DFS rispecchia fedelmente la struttura delle chiamate ricorsive DFS-VISIT. Più
precisamente, si ha che u= π[v] se e solo se DFS-VISIT(v) è stata chiamata durante la visita della lista di
adiacenza di u.
Un’altra importante proprietà della visita in profondità è che i tempi di scoperta e di fine visita hanno una
struttura di parentesi , nel senso che se si rappresentano la scoperta di un vertice u con una parentesi
sinistra “(u” e la fine della sua visita con un parentesi destra “u)”, allora la storia degli eventi di scoperta e di
fine visita di tutti i vertici costituisce una espressione ben formata, nel senso che le parentesi sono
correttamente bilanciate. Il teorema che segue enuncia in modo più formale questa proprietà:
Teorema (delle parentesi)
In ogni visita in profondità di un grafo G=(V,E) (orientato o non orientato), per ogni coppia di vertici w e v,
una ed una sola delle seguenti condizioni è soddisfatta:
nell’albero DFS;
nell’albero DFS.