Capitolo 5 Algoritmi di ricerca su grafo

Capitolo 5
Algoritmi di ricerca su grafo
Gli algoritmi di ricerca su grafo, oggetto dei prossimi paragrafi, rappresentano tecniche
fondamentali per determinare nodi che soddisfino particolari proprietà sia su grafi non
orientati che orientati. Inoltre, le varianti a tali tecniche sono alla base di numerosi importanti algoritmi, come per esempio: trovare tutti i nodi che a partire da un nodo specifico
siano raggiungibili da un path orientato; individuare tutti i path orientati che da tutti i
nodi raggiungano un nodo assegnato; individuare tutte le componenti connesse di un grafo;
determinare se un grafo è bipartito, ecc. Un’ulteriore applicazione di grande riscontro nella
pratica consiste nella ricerca di cicli in grafi orientati e, se il grafo è aciclico, nell’ordinarne
i nodi secondo un dato criterio, cosı̀ come vedremo nella Sezione 5.2.
In merito ai grafi non orientati vedremo, specificamente nella nella Sezione 5.3, gli
algoritmi di ricerca del minimo albero ricoprente di un grafo, all’analisi dei quali faremo
precedere la dimostrazione delle condizioni che garantiscono l’ottimalità della soluzione
trovata.
5.1
Algoritmi di ricerca su grafo
Il problema che ci poniamo di risolvere è il seguente: determinare tutti i nodi in un grafo
orientato G(V, E) raggiungibili lungo cammini orientati a partire da un nodo assegnato s,
chiamato sorgente.
61
62
CAPITOLO 5. ALGORITMI DI RICERCA SU GRAFO
Partire dalla sorgente ed identificare un numero via via crescente di nodi raggiungi-
bili dalla sorgente rappresenta un criterio che possiamo seguire per risolvere il problema.
Immaginiamo che ad ogni passo intermedio dell’algoritmo i nodi del grafo possono trovarsi in due stati differenti: marcato, se è possibile raggiungerli a partire dalla sorgente,
o non marcato se non è stato ancora possibile. È evidente che, se un nodo i è marcato
(e quindi raggiungibile dalla sorgente) ed il nodo j non è ancora marcato ed esiste l’arco
(i, j), allora possiamo marcare j perché allora questo è raggiungibile da un cammino dalla
sorgente attraverso l’arco (i, j). Chiameremo l’arco (i, j) ammissibile se i è marcato e j
non è marcato, non ammissibile viceversa. Successivamente, esaminando archi ammissibili,
potremo marcare altri nodi e, ogni volta che ci riusciremo, affermeremo che il nodo i è un
predecessore di j. L’algoritmo termina quando il grafo non contiene più archi ammissibili.
L’algoritmo visita i nodi marcati in un certo ordine di cui possiamo tenere traccia
utilizzando un vettore che chiameremo ordine e nel quale l’elemento ordine(i) contiene il
passo nel quale i è stato marcato.
In Figura 5.1 viene riportato il listato in pseudocodice dell’algoritmo di ricerca.
Nell’algoritmo in questione, il vettore LISTA contiene l’insieme dei nodi marcati che devono ancora essere considerati per individuare altri archi dai quali poi marcare altri nodi,
mentre il vettore pred(i) definisce un albero di nodi marcati che prende il nome di albero
di ricerca. Quando l’algoritmo sarà terminato, tutti i nodi raggiungibili dalla sorgente
attraverso un cammino orientato risulteranno marcati. Notare che in talune istanze di
grafo l’algoritmo potrebbe terminare senza aver marcato tutti i nodi del grafo stesso, il
che equivale a dire che i nodi non marcati sono tutti quelli non raggiungibili dalla sorgente
tramite un cammino orientato.
Si può dimostrare facilmente che l’algoritmo ha una complessità computazionale di
O(m + n) = O(m). Infatti, ad ogni iterazione del ciclo while o viene trovato un arco
ammissibile oppure no. Nel primo caso, l’algoritmo marca un nuovo nodo e lo aggiunge a
LISTA, mentre nel secondo caso cancella un nodo marcato da LISTA. Pertanto, dato che
ogni nodo viene marcato al più una volta, il ciclo while è eseguito 2n volte. Per quanto
riguarda la ricerca di archi ammissibili, per ogni nodo i viene percorsa la lista di adiacenza
5.1. ALGORITMI DI RICERCA SU GRAFO
63
algorithm SEARCH;
begin
poni tutti nodi come non marcati;
marca la sorgente s;
pred(s) = 0;
next:=1;
ordine(s) = 1;
LISTA := {s};
while LISTA 6= ∅ do
begin
seleziona un nodo i in LISTA;
if il nodo i è incidente ad un arco ammissibile (i, j) then
begin
marca il nodo j;
pred(j) := i;
next:= next + 1;
ordine(j) := next;
aggiungi il nodo j a LISTA;
end
else cancella il nodo i da LISTA;
end
end
Figura 5.1: L’algoritmo di ricerca su grafo.
A(i) al più una volta; quindi l’algoritmo esamina un totale di
P
i∈V
|A(i)| = m archi e
quindi termina al più in O(m) volte.
Un aspetto centrale dell’algoritmo è la disciplina con la quale selezioniamo i nodi del
grafo contenuti nel vettore LISTA per ricercare archi ammissibili e, quindi, marcare nuovi
nodi. Per ottenere ciò possiamo fare uso della lista di adiacenza A(i) introdotta nella
Sezione 3.1 alla quale aggiungiamo la seguente regola d’ordinamento: gli archi in A(i) sono
disposti in ordine crescente rispetto al loro nodo successore, ovvero se (i, j) e (i, k) sono
due nodi consecutivi in A(i), allora j < k.
Tra tutte le discipline possibili di selezione di una lista, le due più note sono la coda e
la pila che definiscono, rispettivamente, le tecniche di ricerca in ampiezza e di ricerca in
profondità.
64
CAPITOLO 5. ALGORITMI DI RICERCA SU GRAFO
2
5
1
6
3
4
Figura 5.2: Grafo per gli Esempi 5.1.1 e 5.1.2.
Ricerca in ampiezza
Nel caso della ricerca in ampiezza viene implementata una disciplina di tipo first in, first out (FIFO), ovvero ogni nuovo elemento viene inserito in coda e l’estrazione avviene
considerando il primo elemento della lista.
Se definiamo la distanza di un nodo i dalla sorgente come la somma degli archi di cui
è composto il cammino diretto da s a i, questa disciplina porta a marcare prima i nodi a
distanza 1, poi quelli a distanza 2 e cosı̀ via.
Proprietà 5.1.1 Nella ricerca in ampiezza, l’albero dei cammini dalla sorgente s ad ogni
nodo i è composto dai cammini più corti.
Esempio 5.1.1 Considerando il grafo di Figura 5.2, si determini l’albero dei cammini
utilizzando la disciplina di ricerca in ampiezza.
Per risolvere il nostro problema, analizziamo passo per passo l’evoluzione dell’algoritmo.
Chiaramente, si devono inizializzare le variabili secondo i valori che seguono: LISTA =
{1}, pred(1) = 0; next = 1; ordine(1) = 1.
Step 1 Al primo passo, selezionando il nodo 1 preso da LISTA, possiamo marcare il nodo 2;
quindi, pred(2) = 1; next = 2; ordine(2) = 2. Dopo l’ultimo aggiornamento, LISTA
= {1, 2} (notare che il nodo 2 è stato aggiunto in coda a LISTA).
5.1. ALGORITMI DI RICERCA SU GRAFO
65
Step 2 Al secondo passo, selezionando il nodo 1 preso da LISTA, possiamo marcare il nodo
3; pred(3) = 1; next = 3; ordine(3) = 3. Dopo l’ultimo aggiornamento, LISTA
= {1, 2, 3}.
Step 3 Al terzo passo, selezionando il nodo 2 preso da LISTA (notare che il nodo 1 è stato
cancellato dalla lista in quanto non presentava più archi ammissibbili ed è quindi stato
selezionato il primo nodo che era stato immesso, cioè il nodo 2) possiamo marcare il
nodo 4; pred(4) = 2; next = 4; ordine(4) = 4. Dopo l’ultimo aggiornamento, LISTA
= {2, 3, 4}.
Step 4 Al quarto passo, selezionando il nodo 2 preso da LISTA, possiamo marcare il nodo
5; pred(5) = 2; next = 5; ordine(5) = 5. Dopo l’ultimo aggiornamento, LISTA
= {2, 3, 4, 5}.
Step 5 Al quinto ed ultimo passo, selezionando il nodo 4 preso da LISTA, possiamo marcare
il nodo 6; pred(6) = 4; next = 6; ordine(6) = 6. Dopo l’ultimo aggiornamento,
LISTA = {4, 5, 6}.
A questo punto l’algoritmo non trova più archi ammissibili e si arresta. In Figura 5.3 è
disegnato l’albero dei cammini definito dall’algoritmo ed i valori assunti dai due vettori
pred(i) e ordine(i).
È da notare che l’applicazione della disciplina FIFO ha originato, come ci dovevamo
aspettare, un albero con le minime distanze tra la sorgente s ed ogni nodo di G.
s
Figura 5.3: Albero dei cammini e valori dei vettori pred(i) e ordine(i) nel caso FIFO.
66
CAPITOLO 5. ALGORITMI DI RICERCA SU GRAFO
Ricerca in profondità
Nel caso della ricerca in profondità viene implementata una disciplina del tipo last in,
first out (LIFO), ovvero ogni elemento nuovo viene inserito in coda e l’estrazione avviene
considerando l’ultimo elemento della lista.
Utilizzando questa disciplina si crea un cammino orientato il più lungo possibile e
quando non si riescono ad individuare altri archi ammissibili, la lista viene scorsa ritornando
verso i nodi che erano stati inseriti nei passi precedenti.
Proprietà 5.1.2 Se il nodo j è un successore del nodo i e i 6= j, allora ordine(j) >
ordine(i). Inoltre, tutti i successori dei nodi nella sequenza sono ordinati consecutivamente.
Esempio 5.1.2 Considerando il grafo di Figura 5.2, si determini l’albero dei cammini
utilizzando la disciplina di ricerca in profondità.
Anche in questo caso analizziamo passo per passo l’evoluzione dell’algoritmo. Chiaramente, si devono inizializzare le variabili secondo i valori che seguono: LISTA = {1},
pred(1) = 0; next = 1; ordine(1) = 1.
Step 1 Al primo passo, selezionando il nodo 1 preso da LISTA, possiamo marcare il nodo 2;
pred(2) = 1; ordine(2) = 2. Dopo l’ultimo aggiornamento, LISTA = {1, 2}.
Step 2 Al secondo passo, selezionando il nodo 2 preso da LISTA, possiamo marcare il nodo 3
(notare che adesso viene selezionato l’ultimo nodo che era entrato in LISTA, appunto
il nodo 2); pred(3) = 2; ordine(3) = 3. Dopo l’ultimo aggiornamento, LISTA =
{1, 2, 3}.
Step 3 Al terzo passo, selezionando il nodo 3 preso da LISTA, possiamo marcare il nodo 4;
pred(4) = 3; ordine(4) = 4. Dopo l’ultimo aggiornamento, LISTA = {1, 2, 3, 4}.
Step 4 Al quarto passo, selezionando il nodo 4 preso da LISTA, possiamo marcare il nodo
6; pred(6) = 4; ordine(6) = 5. Dopo l’ultimo aggiornamento, LISTA = {1, 2, 3, 4, 6}.
5.2. ALGORITMO DI ORDINAMENTO TOPOLOGICO
67
Step 5 Al quinto ed ultimo passo, selezionando il nodo 2 preso da LISTA, possiamo marcare
il nodo 5; pred(5) = 2; ordine(5) = 6. Dopo l’ultimo aggiornamento, LISTA =
{1, 2, 5}.
A questo punto l’algoritmo non trova più archi ammissibili e si arresta. In Figura 5.3 è
tracciato l’albero dei cammini definito dall’algoritmo ed i valori assunti dai due vettori
pred(i) e ordine(i).
Osservando l’albero dei cammini individuato, possiamo notare come questa volta il
cammino dalla sorgente ai nodi sia il più lungo possibile.
s
Figura 5.4: Albero dei cammini e valori dei vettori pred(i) e ordine(i) nel caso LIFO.
5.2
Algoritmo di ordinamento topologico
Il prossimo problema che affronteremo è quello di cercare, se esistono, cicli diretti in un
grafo orientato, altrimenti, se il grafo è aciclico, di etichettare i nodi 1, 2, . . . , n in modo tale
che per ogni arco (i, j) ∈ E(G), l’etichetta del nodo i sia minore dell’etichetta di j. Questa
numerazione, se esiste, prende il nome di ordinamento topologico. Gli algoritmi di
ordinamento topologico sono essenziali in molte applicazioni come per esempio nel project
management.
Per etichettare i nodi di un grafo G con n numeri distinti possiamo usare un vettore
ordine in modo che ordine(i) fornisca l’etichetta del nodo i.
68
CAPITOLO 5. ALGORITMI DI RICERCA SU GRAFO
Definizione 5.2.1 Dato un grafo orientato G, diremo che una etichettatura è un
ordinamento topologico se ∀(i, j) ∈ E, si ha che ordine(i) < ordine(j).
Come si è detto, non tutti i grafi possono essere ordinati topologicamente; basta infatti che
il grafo sia ciclico e la condizione ordine(i) < ordine(j) non può essere dimostrata vera
per ogni arco del grafo. Analogamente, un grafo che non contiene cicli può essere ordinato
topologicamente. Questo ci porta a dire che un grafo orientato è aciclico se e soltanto se
ammette un ordinamento topologico.
algorithm ORDINAMENTO TOPOLOGICO;
begin
for tutti i nodi i ∈ V do
indegree(i):=0;
for tutti gli archi (i, j) ∈ E do
indegree(j):=indegree(j) + 1;
LISTA= 0;
next:=0;
for tutti i nodi i ∈ V do
if indegree(i) = 0 then LISTA = LISTA∪{i};
while LISTA 6= ∅ do
begin
seleziona un nodo i in LISTA e rimuovilo;
next:= next + 1;
ordine(i) := next;
for tutti gli archi (i, j) ∈ E do
begin
indegree(j):= indegree(j) − 1;
if indegree(j) = 0 then LISTA = LISTA∪{j};
end
end
if next < n then il grafo contiene un ciclo diretto;
else il grafo è aciclico ed il vettore ordine contiene il suo
ordinamento topologico;
end
Figura 5.5: L’algoritmo di ordinamento topologico.
Vediamo ora una implementazione dell’algoritmo di ordinamento topologico, di cui
forniamo lo pseudocodice in Figura 5.5. La variabile indegree(i) considera il numero di
archi entranti nel nodo i, che viene chiamato anche grado entrante di un nodo. In analogia,
5.2. ALGORITMO DI ORDINAMENTO TOPOLOGICO
69
2
1
5
7
6
8
3
4
Figura 5.6: Grafo per l’Esempio 5.2.1
si può contare anche il numero di archi uscenti da un nodo che prende il nome di grado
uscente.
La complessità computazionale di questo algoritmo è O(m). Infatti, per prima cosa,
vengono conteggiati i gradi in ingresso di tutti i nodi, formando una lista che comprende
tutti i nodi con grado 0. Ad ogni iterazione selezioniamo un nodo i da LISTA, per ogni
arco (i, j) ∈ A(i) riduciamo il grado in ingresso di 1 del nodo j e se il grado in ingresso
del nodo j diviene 0 lo aggiungiamo a LISTA. Dato che l’algoritmo esamina ogni nodo ed
ogni arco del grafo O(1) volte, allora la sua complessità totale e O(m).
Esempio 5.2.1 Dato il grafo in Figura 5.6, individuare, se esiste, un ordinamento
topologico.
L’algoritmo comincia inserendo al primo passo il nodo 1 nella LISTA e quindi ordine(1) =
1. Al successivo passo, in LISTA ci sono i nodi 3 e 4, scelgo il primo e aggiorno il vettore
ordine(3) = 2. Continuando, in LISTA ci sono i nodi 2 e 4, scelgo ancora il primo ed
ho ordine(2) = 3. Continuando in questo modo, ottengo la sequenza ordine(4) = 4,
ordine(6) = 5, ordine(5) = 6, ordine(7) = 7 ed infine ordine(8) = 8. In Figura 5.7 è
rappresentato il vettore ordine(i). Si può notare che tale ordinamento trovato non è unico.
Infatti, in alcuni passi si possono presentare più di un nodo con grado in ingresso nullo e
la scelta di uno e di un altro non pregiudica la soluzione perché la relazione ordine(i) <
ordine(j) rimane valida, ∀(i, j) ∈ E.
70
CAPITOLO 5. ALGORITMI DI RICERCA SU GRAFO
Figura 5.7: Contenuto del vettore ordine(i) dopo l’ordinamento topologico.
Infine, siccome l’algoritmo termina con una etichettatura ammissibile per ogni nodo, si
può affermare che il grafo assegnato è aciclico.
È importante ribadire che nel caso dell’ordinamento topologico la selezione dell’elemento
dalla lista può essere effettuato arbitrariamente. Questo porta a determinare differenti
etichettature, cosa del tutto lecita visto che il nostro obiettivo era quello di rispettare una
semplice condizione, ovvero che ∀(i, j) ∈ E, si abbia che ordine(i) < ordine(j).
5.3
Algoritmi di ricerca di alberi ricoprenti minimi
Nella Sezione 3.4 abbiamo dato la definizione di albero ricoprente, definendolo come l’albero
che comprende tutti i nodi di un dato grafo connesso G. Chiaramente un albero ricoprente
non è, in generale, unico, ma dipende dalla scelta degli archi dell’insieme E.
In questa sezione vogliamo considerare il Problema del minimo albero ricoprente. Supponiamo sia assegnato un grafo connesso non orientato G = (V, E) e che ad ogni arco
(i, j) ∈ E sia assegnato un costo cij , dove cij è un numero intero. Si vuole trovare un albero ricoprente T chiamato minimo albero ricoprente (in inglese: minimum spanning
tree, MST) tale che il costo totale, dato dalla somma dei singoli costi degli archi che lo
costituiscono, sia minimo.
Il problema del minimo albero ricoprente ha una importanza rilevante dato il vasto
campo di applicazioni che vanno dal disegno di sistemi fisici, alla cluster analysis fino ai
metodi di riduzione della memorizzazione dei dati.
5.3. ALGORITMI DI RICERCA DI ALBERI RICOPRENTI MINIMI
71
Condizioni di ottimalità
Per risolvere il problema forniremo due condizioni di ottimalità che ci porteranno a presentare due algoritmi in grado di fornirci la soluzione ottima. Chiaramente le due condizioni
sono equivalenti, cosı̀ come i due algoritmi forniscono la stessa soluzione sullo stesso grafo.
Prima di definire tali condizioni esponiamo alcune osservazioni preliminari:
Definizione 5.3.1 Dato un grafo G = (V ; E), si definisce taglio di G una sua partizione
in due insiemi, S ed S = V − S. Ogni taglio definisce un insieme di archi che hanno un
estremo in S e l’altro in S. Indicheremo il taglio con la notazione [S, S].
Nell’esposizione successiva faremo spesso riferimento alle due osservazioni seguenti:
1. Per ogni arco (p, q) che non appartiene all’albero ricoprente T , allora T contiene un
unico path da p a q. L’arco (p, q) assieme al path definisce un ciclo.
2. Se si cancella un arco (i, j) ∈ T , il grafo risultante partiziona l’insieme V in due
sottoinsiemi. Gli archi del grafo G sottostante i cui estremi ricadono uno in un
sottoinsieme e uno nell’altro definiscono un taglio.
S
S
T∗
i
p
j
q
Figura 5.8: Tagli e path per le condizioni di ottimalità.
In Figura 5.8 sono illustrate le due osservazioni che abbiamo esposto, ovvero l’arco
(p, q) genera con il path pq un ciclo, mentre la rimozione dell’arco (i, j) genera il taglio
[S, S]. Risulterà molto utile al lettore fare riferimento a tale figura per le dimostrazioni che
seguono.
72
CAPITOLO 5. ALGORITMI DI RICERCA SU GRAFO
Enunciamo la prima condizione di ottimalità, che prende il nome di condizione di
ottimalità sul taglio:
Teorema 5.3.1 Un albero ricoprente è un minimo albero ricoprente T ? se e soltanto se è
soddisfatta la seguente condizione di ottimalità sul taglio: per ogni arco (i, j) ∈ T ∗ allora
cij ≤ cpq , per ogni arco (p, q) contenuto nel taglio formato dalla rimozione dell’arco (i, j)
da T ∗ .
Dimostrazione: Per dimostrare che ogni albero minimo ricoprente T ? deve soddisfare la
condizione di ottimalità del taglio basta notare che, se esistesse un arco (p, q) ∈ [S, S] con
cpq < cij , basterebbe sostituire tale arco all’arco (i, j) per ottenere un nuovo albero T ◦ con
un costo minore di T ? , contraddicendo la sua ottimalità.
Per dimostrare la sufficienza, dobbiamo far vedere che ogni albero T ? che soddisfa
la condizione di ottimalità deve essere ottimo. Supponiamo allora che T ◦ 6= T ? sia un
minimo albero ricoprente. T ? contiene almeno un arco (i, j), che non appartiene a T ◦ che,
se cancellato, crea un taglio [S, S]. Se ora aggiungiamo tale arco a T ◦ creiamo un ciclo W
che conterrà un arco (p, q) ∈ [S, S]. Dato che T ? soddisfa la condizione di ottimalità, allora
avremo che cij ≤ cpq , ma contemporaneamente T ◦ è un albero minimo ricoprente, quindi
cpq ≤ cij ; di conseguenza deve essere che cij = cpq . Chiaramente, sostituire l’arco (p, q)
all’arco (i, j) in T ? non comporta alcun aumento del costo di tale albero, ma otteniamo un
nuovo T ? che ha un ulteriore arco in comune con T ◦ . Ripetendo tale procedura per tutti i
suoi archi, otterremo un nuovo T ? che coincide con il minimo albero ricoprente T ◦ , ovvero
anche T ? è un minimo albero ricoprente.
La condizione di ottimalità sul taglio implica che ogni arco di un minimo albero ricoprente ha il valore del costo più piccolo tra tutti i costi degli archi che appartengono al
taglio generato dalla sua rimozione. Questo implica che noi possiamo includere nell’albero
ricoprente minimo qualunque arco a costo minimo appartente a qualunque taglio. Per
dimostrare questo ci è utile la seguente proposizione:
Proposizione 5.3.2 Sia F un sottoinsieme di archi appartenenti ad un albero ricoprente
minimo e sia S un insieme di nodi di certe componenti di F . Si supponga che (i, j) sia un
5.3. ALGORITMI DI RICERCA DI ALBERI RICOPRENTI MINIMI
73
arco a costo minimo nel taglio [S, S]. Allora un minimo albero ricoprente contiene tutti gli
archi di F e l’arco (i, j).
Dimostrazione: Supponiamo che F ⊆ T ? . Se (i, j) ∈ T ? allora la proposizione è banalmente vera. Se (i, j) ∈
/ T ? , aggiungendolo a T ? si crea un ciclo W che contiene almeno
un arco (p, q) 6= (i, j), con (p, q) ∈ [S, S]. Per assunzione, cij ≤ cpq , ma T ? soddisfa la
condizione di ottimalità e quindi cij ≥ cpq , quindi cij = cpq . Di conseguenza, aggiungere
l’arco (i, j) e rimuovere l’arco (p, q) produce un albero minimo ricoprente che contiene tutti
gli archi di F e l’arco (i, j).
Passiamo ora a dimostrare la seconda condizione di ottimalità che chiameremo
condizione di ottimalità sul path:
Teorema 5.3.3 Un albero ricoprente è un minimo albero ricoprente T ? se e soltanto se è
soddisfatta la seguente condizione di ottimalità sul path: per ogni arco (p, q) ∈ G che non
appartiene a T ? si ha che cij ≤ cpq , ∀(i, j) contenuto nel path in T ? che connette i nodi p
e q.
Dimostrazione: Per dimostrare la necessità, supponiamo che T ? sia un albero minimo
ricoprente e che (i, j) sia un arco contenuto nel path tra i nodi p e q. Se cij > cpq , allora
la sostituzione di tale arco in T ? al posto dell’arco (i, j) creerebbe un nuovo albero T ◦ con
un costo minore di T ? , contraddicendo la sua ottimalità.
Per dimostrare che se T ? è ottimo allora deve verificare la condizione di ottimalità sul
path, supponiamo che (i, j) sia un arco in T ? e siano S e S i due insiemi di nodi connessi
generati dalla cancellazione dell’arco (i, j) da T ? , con i ∈ S e j ∈ S. Consideriamo ora un
arco (p, q) ∈ [S, S]. Dato che T ? contiene un unico path tra p e q e dato che (i, j) è l’unico
arco tra S ed S, allora (i, j) deve appartenere a tale path. La condizione di ottimalità
implica che cij ≤ cpq e dato che tale condizione deve essere vera per ogni arco (p, q) ∈
/ T?
nel taglio [S, S] generato dalla cancellazione dell’arco (i, j), allora T ? soddisfa la condizione
di ottimalità sul taglio e quindi deve essere ottimo.
Si può notare come nella dimostrazione di quest’ultimo teorema si sia usata quella della
sufficienza del Teorema 5.3.1. Questo evidenzia come le due condizioni siano completamen-
74
CAPITOLO 5. ALGORITMI DI RICERCA SU GRAFO
te equivalenti e che quindi un minimo albero ricoprente T ? che soddisfa la condizione di
ottimalità sul taglio deve contemporaneamente soddisfare anche la condizione di ottimalità
sul path.
Un’altra osservazione è che la condizione di ottimalità sul taglio è una condizione che
potremmo indicare come una caratteristica esterna del minimo albero ricoprente, nel senso
che coglie una relazione tra un suo arco e gli altri archi fuori dall’albero; viceversa, la
condizione di ottimalità sul path coglie una caratterizzazione interna del minimo albero
ricoprente che considera la relazione tra un singolo arco, che non appartiene all’albero, ed
i diversi archi che appartengono al path, nell’albero, che si forma aggiungendo tale arco.
5.3.1
Algoritmo di Kruskal
La condizione di ottimalità sul path suggerisce immediatamente un algoritmo per la ricerca
del minimo albero ricoprente su un grafo. Infatti, si può partire con un generico albero
ricoprente T e si testa la condizione di ottimalità sul path; se T soddisfa la condizione per
ogni arco allora è un albero ottimo, altrimenti ci sarà un qualche arco (p, q) che chiude
l’unico path tra p e q in T con cij > cpq . Sara sufficiente sostituire l’arco (p, q) all’arco (i, j)
per ottenere un albero di costo più basso; quindi, ripetendo la procedura, in un numero
finito di iterazioni avremo l’albero ottimo. La semplicità di questo algoritmo ha però
una complessità computazionale che non può essere limitata in un numero polinomiale di
iterazioni.
Per ottenere un algoritmo più efficiente, noto con il nome di Algoritmo di Kruskal ,
si può iniziare considerando un ordinamento non decrescente degli archi secondo il loro
peso e, quindi, una procedura che a partire da un albero vuoto, rispettando l’ordinamento
posto, aggiunga archi uno alla volta. L’algoritmo può essere descritto nel seguente modo:
Algoritmo di Kruskal
Step 1 - Ordina tutti gli archi del grafo G per valori non decrescenti del costo degli archi;
Step 2 - Prendi una foresta ricoprente H = (V (H), F ) di G, con all’inizio F = ∅;
5.3. ALGORITMI DI RICERCA DI ALBERI RICOPRENTI MINIMI
75
Step i - Aggiungi ad F un arco (i, j) ∈
/ F a minimo costo, tale che H rimanga una foresta.
Stop quando H è un albero ricoprente.
L’algoritmo esegue la procedura corretta perché, ad ogni passo, vengono scartati archi
che formerebbero un ciclo con quelli che invece appartengono alla foresta corrente. Inoltre,
si può notare che gli archi che formerebbero il ciclo hanno un costo che è maggiore o al più
uguale al costo degli altri archi, appartenenti alla foresta, nel ciclo; questo perché i costi
sono ordinati in ordine non decrescente. Pertanto, l’albero trovato soddisfa la condizione
di ottimalità sul path e quindi è ottimo.
Esempio 5.3.1 Dato il grafo G = (V, E) riportato in Figura 5.9 ed assegnati i costi cij ,
∀(i, j) ∈ E (trascritti accanto ad ogni arco) trovare l’albero ricoprente ottimo mediante
l’algoritmo di Kruskal e fornire il suo valore ottimo z ? .
1
c12 = 5
2
18
15
10
12
5
16
8
3
4
Figura 5.9: Grafo per l’Esempio 5.3.1.
Il primo passo è quello di ordinare gli archi in ordine non decrescente, attraverso il quale
otteniamo la lista {(1, 2), (3, 4), (2, 3), (2, 4), (1, 3), (4, 5), (2, 5)}. Il passo successivo (Step 1
in Figura 5.10) parte dalla foresta F = ∅ e poi seleziona l’arco (1, 2) che è quello a costo
minimo e lo aggiunge ad F . Al passo successivo (Step 2 in Figura 5.10) viene selezionato
(3, 4) e dopo aver controllato che non crei un ciclo, è aggiunto ad F . L’algoritmo procede
selezionando l’arco (2, 3) (Step 3 in Figura 5.10) e, dopo aver controllato che non si crei
un ciclo, lo aggiunge ad F. Il test sul ciclo fallisce nei due passi successivi (Step 4 e 5 in
Figura 5.10) quando selezionando prima l’arco (2, 4) e poi l’arco (1, 3) ci si accorge che
76
CAPITOLO 5. ALGORITMI DI RICERCA SU GRAFO
generano un ciclo. L’ultimo passo (Step 6 in Figura 5.10) seleziona l’arco (4, 5) e con
quest’ultimo inserimento la foresta F diventa l’albero ricoprente ottimo T ? , avente costo
z ? = 39.
Step 1
Step 2
Step 3
Step 4
Step 5
Step 6
Figura 5.10: Evoluzione dell’algoritmo di Kruskal sul grafo di Figura 5.9.
Per determinare la complessità computazionale dell’algoritmo1 occorre osservare che,
per individuare un ciclo, si può, tra l’altro, creare ad ogni passo delle liste di nodi in numero
pari alle componenti della foresta ricoprente corrente (nell’esempio precedente al passo 3
avremmo le due liste {1, 2, 3, 4} e {5}) e poi, per ogni arco (i, j) che si vuole introdurre,
controllare se i nodi i e j appartengono ad una stessa lista. Se il test ha esito positivo,
l’arco crea un ciclo (come l’arco (2, 4) al passo 4 dell’esempio) e viene scartato, altrimenti
può essere aggiunto alla foresta corrente (come l’arco (4, 5) all’ultimo passo dell’esempio).
Per eseguire questi passi occorre effettuare O(n) iterazioni per ogni arco del grafo e, quindi,
la complessità computazionale dell’algoritmo di Kruskal è O(nm).
5.3.2
Algoritmo di Prim
Il secondo algoritmo per la ricerca del minimo albero ricoprente segue dalla applicazione
della condizione di ottimalità sul taglio. L’algoritmo, noto con il nome di Algoritmo
di Prim, costruisce un albero ricoprente a partire da un nodo ed aggiunge un arco per
1
Il calcolo è eseguito a meno dell’ordinamento dei costi degli archi che, in un grafo di ordine n,
dimensione m e costi di valore arbitrario, è uguale a O(m log m) = O(m log n2 ) = O(m log n).
5.3. ALGORITMI DI RICERCA DI ALBERI RICOPRENTI MINIMI
77
volta. L’arco viene selezionato tra quelli a costo minimo appartenenti al taglio [S, S]
generato dall’inserimento in S dei nodi estremi degli archi aggiunti all’albero nel passo
precedente. L’algoritmo termina quando S = V . La correttezza dell’algoritmo segue dalla
Proposizione 5.3.2 dove è stato dimostrato che ogni arco che aggiungiamo ad un albero
è contenuto in un certo albero ricoprente assieme agli archi che sono stati selezionati nei
passi successivi. La descrizione dell’algoritmo è la seguente:
Algoritmo di Prim
Step 1 - Considerare un albero H = (V (H), T ), con inizialmente V (H) = {r} e T = ∅, con
r ∈ V (G);
Step i - Ad ogni passo, aggiungi all’albero connesso H un arco a minimo costo (i, j) ∈
/ H
scegliendolo tra quelli che a minimo costo aggiungono un nuovo nodo e mantengono
l’albero connesso. Stop se H è il minimo albero ricoprente.
In pratica, l’algoritmo evolve attraverso la selezione di archi a minimo costo che abbiano gli estremi uno nell’insieme dei nodi degli archi già selezionati (cioè in S) e l’altro
nell’insieme dei nodi degli archi che non sono stati ancora selezionati (cioè in S). Quindi,
l’algoritmo mantiene un albero connesso che cresce di un arco ad ogni passo e che, all’ultimo, coincide con l’albero ottimo. Osservando l’algoritmo di Kruskal si nota invece che
l’algoritmo aggiunge, ad ogni passo, archi ad una foresta che diventa connessa solo alla
fine.
Per analizzare la complessità computazionale dell’algoritmo, basta notare che l’algoritmo stesso esegue n − 1 iterazioni per definire gli n − 1 archi dell’albero ed in ogni iterazione
viene selezionato da un taglio un arco a costo minimo. Dato che tale selezione può essere
eseguita anche sull’intera lista degli archi, ne segue che la complessità dell’algoritmo di
Prim è O(nm).
Esempio 5.3.2 Considerando il grafo di Figura 5.9, trovare l’albero ricoprente ottimo
mediante l’algoritmo di Prim e fornire il suo valore ottimo z ? .
78
CAPITOLO 5. ALGORITMI DI RICERCA SU GRAFO
Supponiamo che l’algoritmo parta da V (H) = {1}. In questo caso, l’algoritmo può scegliere tra gli archi del taglio [S, S] = [{1}, {2, 3, 4, 5}]; l’arco a costo minimo selezionato e che
mantiene l’albero connesso è (1, 2) (vedi Step 1 in Figura 5.11, dove la linea tratteggiata
indica il taglio); aggiungo l’arco ad H (indicato in figura dagli archi in grassetto) ed il
nodo 2 ad S. Al passo successivo (Step 2 in Figura 5.11) l’algoritmo deve scegliere gli archi
a costo minimo tra quelli del taglio [{1, 2}, {3, 4, 5}] e, tra tutti, viene scelto l’arco (2, 3).
L’algoritmo continua scegliendo (3, 4) in [{1, 2, 3}, {4, 5}] (Step 3 in Figura 5.11) ed infine
termina aggiungendo l’arco (4, 5) dal taglio [(1, 2, 3, 4}, {5}] (Step 4 in Figura 5.11), restituendoci l’albero ottimo di costo minimo, pari a z ? = 39. Come si può notare, l’algoritmo
termina in n − 1 = 4 passi, avendo ad ogni passo aggiunto un arco ad un albero connesso.
Step 1
Step 2
Step 3
Step 4
Figura 5.11: Evoluzione dell’algoritmo di Prim sul grafo di Figura 5.9.
Osservazione. Si vuole ribadire che negli esempi che abbiamo esposto, cosı̀ come ci dovevamo aspettare, gli algoritmi forniscono lo stesso valore ottimo e, inoltre, i due alberi
ottenuti sono identici. Quest’ultima affermazione non nasconde però una proprietà di unicità sugli archi che compongono l’albero ricoprente ottimo, perché, in generale, si possono
ottenere alberi a costo minimo composti da archi differenti. Questo non ci deve sorprendere
poiché gli algoritmi si possono trovare nella condizione di poter selezionare archi nella lista
(per Kruskal) o nel taglio (per Prim) che abbiano stesso costo. Dato che selezionare un
arco piuttosto che un altro è solo funzione delle condizioni di ottimalità, possiamo ottenere
alberi minimi ottimi composti da archi differenti.
5.4. ESERCIZI
5.4
79
Esercizi
Da inserire.
Es. 5.4.1 Data le matrice di adiacenza dell’Esercizio 3.7.2, disegnare il grafo corrispondente e individuare su questo, se esiste, un ordinamento topologico. In caso negativo,
motivare perché esso non esiste e quindi rimuovere il minimo numero di archi in modo che
sia possibile trovare un ordinamento topologico.
80
CAPITOLO 5. ALGORITMI DI RICERCA SU GRAFO