Universit`a degli Studi di Roma “Tor Vergata” GRAFI E RETI DI

annuncio pubblicitario
Università degli Studi di Roma
“Tor Vergata”
Dispense per il corso di
GRAFI E RETI DI FLUSSO
Antonio Iovanella
Ottobre 2006
I
Indirizzo dell’autore:
Antonio Iovanella
Dipartimento di Ingegneria dell’Impresa,
Università di Roma “Tor Vergata”
Via del Politecnico 1, 00133 Roma - Italia
Email: [email protected]
Web Page: http://www.disp.uniroma2.it/Users/iovanella/
Home page della didattica: http://www.uniroma2.it/didattica/grfbis
Indice
1 Introduzione
1.1
1.2
1.3
1
Definizioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
1.1.1
Sottografi, ordine, dimensione e densità di un grafo . . . . . . . . .
4
1.1.2
Vicinati, gradi e regolarità . . . . . . . . . . . . . . . . . . . . . . .
7
1.1.3
k-Fattorizzazioni . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
Grafi e modelli . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
1.2.1
Relazioni e sottografi . . . . . . . . . . . . . . . . . . . . . . . . . .
9
1.2.2
Assegnamento di lavori . . . . . . . . . . . . . . . . . . . . . . . . .
10
1.2.3
Scheduling di team in una azienda
. . . . . . . . . . . . . . . . . .
11
1.2.4
Percorsi su rete . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
Esercizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
2 Algoritmi
17
2.1
Breve tassonomia dei problemi decisionali
. . . . . . . . . . . . . . . . . .
17
2.2
Algoritmi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
18
2.2.1
Algoritmi di ricerca . . . . . . . . . . . . . . . . . . . . . . . . . . .
20
Crescita di funzioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
22
2.3.1
Notazione Big-O . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23
2.3.2
Crescita di combinazioni di funzioni . . . . . . . . . . . . . . . . . .
24
2.4
Complessità degli algoritmi . . . . . . . . . . . . . . . . . . . . . . . . . . .
26
2.5
Esercizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
30
2.3
II
INDICE
III
3 Proprietà e strutture dei grafi
32
3.1
Strutture dati per la rappresentazione di grafi . . . . . . . . . . . . . . . .
32
3.2
Isomorfismo tra grafi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
36
3.3
Connessione, cicli e grafi bipartiti . . . . . . . . . . . . . . . . . . . . . . .
39
3.4
Alberi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
44
3.5
Cammini Euleriani . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
45
3.6
Cammini Hamiltoniani . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
47
3.7
Esercizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
47
4 Grafi Planari
50
4.1
Grafi sul piano . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
51
4.2
Grafi duali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
53
4.2.1
Proprietà dei grafi planari e Formula di Eulero . . . . . . . . . . . .
54
4.2.2
Caratterizzazione dei grafi planari . . . . . . . . . . . . . . . . . . .
58
Esercizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
58
4.3
5 Algoritmi di ricerca su grafo
60
5.1
Algoritmi di ricerca su grafo . . . . . . . . . . . . . . . . . . . . . . . . . .
60
5.2
Algoritmo di ordinamento topologico . . . . . . . . . . . . . . . . . . . . .
66
5.3
Algoritmi di ricerca di alberi ricoprenti minimi . . . . . . . . . . . . . . . .
69
5.3.1
Algoritmo di Kruskal . . . . . . . . . . . . . . . . . . . . . . . . . .
73
5.3.2
Algoritmo di Prim . . . . . . . . . . . . . . . . . . . . . . . . . . .
75
Esercizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
78
5.4
6 Problemi di flusso su grafo
6.1
6.2
6.3
79
Algoritmi di ricerca del cammino minimo su di un grafo . . . . . . . . . . .
80
6.1.1
L’algoritmo di Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . .
83
Problemi di massimo flusso su grafo . . . . . . . . . . . . . . . . . . . . . .
88
6.2.1
93
L’algoritmo di Ford e Fulkerson . . . . . . . . . . . . . . . . . . . .
Esercizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Elenco delle figure
1.1
L’attuale mappa dell’area dei ponti del problema di Könisberg [1]. . . . . .
2
1.2
Grafo corrispondente al Problema di Könisberg. . . . . . . . . . . . . . . .
2
1.3
Grafo dell’Esempio 1.1.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
1.4
Un sottografo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
1.5
Un sottografo indotto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
1.6
Un sottografo ricoprente . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
1.7
Esempi di grafi 3-regolari . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
1.8
Le fattorizzazioni di K4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
1.9
Grafo rappresentante le relazioni di conoscenza tra i cinque membri del gruppo 10
1.10 Grafo rappresentante le relazioni di non conoscenza tra i cinque membri del
gruppo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
1.11 Grafo bipartito . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11
1.12 Una istanza del problema dei team . . . . . . . . . . . . . . . . . . . . . .
12
1.13 Un cammino . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14
1.14 Un ciclo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14
1.15 Alcuni esempi di path e cicli . . . . . . . . . . . . . . . . . . . . . . . . . .
14
1.16 Grafo per l’Esempio 1.2.1
. . . . . . . . . . . . . . . . . . . . . . . . . . .
15
2.1
Grafico delle funzioni più comunemente usate nella stima Big-O . . . . . .
26
3.1
Grafo non orientato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
3.2
Grafo orientato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
3.3
Lista di adiacenza per il grafo di Figura 3.2. . . . . . . . . . . . . . . . . .
36
IV
V
ELENCO DELLE FIGURE
3.4
Esempio di due grafi isomorfi . . . . . . . . . . . . . . . . . . . . . . . . .
37
3.5
Due grafi isomorfi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
38
3.6
I grafi bipartiti completi K2,3 e K3,3 . . . . . . . . . . . . . . . . . . . . . .
39
3.7
Un grafo con quattro componenti connesse. . . . . . . . . . . . . . . . . . .
40
3.8
Componente connessa di x . . . . . . . . . . . . . . . . . . . . . . . . . . .
41
3.9
Un circuito con un nodo ripetuto. . . . . . . . . . . . . . . . . . . . . . . .
41
3.10 Una foresta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
44
3.11 Un albero . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
44
3.12 Come spostare i quattro cavalli. . . . . . . . . . . . . . . . . . . . . . . . .
47
4.1
Il problema dei tre nemici. . . . . . . . . . . . . . . . . . . . . . . . . . . .
50
4.2
Connessioni per il problema dei tre nemici. . . . . . . . . . . . . . . . . . .
51
4.3
K5 e K3,3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
52
4.4
La facce di un grafo planare. . . . . . . . . . . . . . . . . . . . . . . . . . .
53
4.5
Costruzione del duale. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
54
4.6
Un esempio di grafo G e del suo duale G∗ . . . . . . . . . . . . . . . . . . .
54
4.7
Esempio di due embedding planari con duali non isomorfi. . . . . . . . . .
55
4.8
Il ciclo C evidenziato per la dimostrazione del teorema. . . . . . . . . . . .
57
4.9
Contrazione dell’arco e. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
57
5.1
L’algoritmo di ricerca su grafo.
. . . . . . . . . . . . . . . . . . . . . . . .
62
5.2
Grafo per gli Esempi 5.1.1 e 5.1.2. . . . . . . . . . . . . . . . . . . . . . .
63
5.3
Albero dei cammini e valori dei vettori pred(i) e ordine(i) nel caso FIFO. .
64
5.4
Albero dei cammini e valori dei vettori pred(i) e ordine(i) nel caso LIFO. .
66
5.5
L’algoritmo di ordinamento topologico. . . . . . . . . . . . . . . . . . . . .
67
5.6
Grafo per l’Esempio 5.2.1
. . . . . . . . . . . . . . . . . . . . . . . . . . .
68
5.7
Contenuto del vettore ordine(i) dopo l’ordinamento topologico. . . . . . .
69
5.8
Tagli e path per le condizioni di ottimalità. . . . . . . . . . . . . . . . . . .
70
5.9
Grafo per l’Esempio 5.3.1. . . . . . . . . . . . . . . . . . . . . . . . . . . .
74
5.10 Evoluzione dell’algoritmo di Kruskal sul grafo di Figura 5.9. . . . . . . . .
75
VI
ELENCO DELLE FIGURE
5.11 Evoluzione dell’algoritmo di Prim sul grafo di Figura 5.9. . . . . . . . . . .
77
6.1
Cammini da s a k. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
82
6.2
L’algoritmo di Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
84
6.3
Grafo per l’Esempio 6.1.1 e corrispondente tabella degli archi. . . . . . . .
85
6.4
Evoluzione delle etichette dei nodi e lista di adiacenza per il grafo di Figura 6.3. 87
6.5
Disegno per la correttezza di Dijkstra . . . . . . . . . . . . . . . . . . . . .
88
6.6
Costruzione del grafo residuo. . . . . . . . . . . . . . . . . . . . . . . . . .
90
6.7
Un grafo G con un flusso assegnato x ed il corrispondente grafo residuo G(x). 91
6.8
Un taglio s-t per il grafo di Figura 6.7. . . . . . . . . . . . . . . . . . . . .
91
6.9
L’algoritmo di Ford e Fulkerson . . . . . . . . . . . . . . . . . . . . . . . .
95
6.10 Grafo per l’Esempio 6.2.1. . . . . . . . . . . . . . . . . . . . . . . . . . . .
96
6.11 Il grafo residuo dopo la prima iterazione. . . . . . . . . . . . . . . . . . . .
96
6.12 Il grafo residuo dopo la seconda iterazione. . . . . . . . . . . . . . . . . . .
97
6.13 Il grafo residuo dopo la terza iterazione. . . . . . . . . . . . . . . . . . . .
97
6.14 Il grafo residuo dopo la quarta iterazione. . . . . . . . . . . . . . . . . . . .
98
6.15 Il taglio minimo del grafo di Figura 6.10. . . . . . . . . . . . . . . . . . . .
98
6.16 Istanza patologica per l’algoritmo di Ford e Fulkerson.
99
. . . . . . . . . . .
PREFAZIONE
VII
Prefazione
Queste dispense nascono per essere affiancate ai testi adottati per i corsi di Grafi e Reti di
Flusso, previsti per gli studenti di Ingegneria Gestionale e per quelli di Ingegneria Online
della Facoltà di Ingegneria dell’Università di Roma Tor Vergata. Esse costituiscono inoltre
un utile strumento di approfondimento per gli studenti del corso di Teoria dei Grafi e Reti
di Flusso del Master di II livello in Ingegneria dei Sistemi a Rete della Facoltà di Ingegneria
dell’Università di Roma Tor Vergata.
Essendo questa la primissima versione delle dispense, conterrà sicuramente degli errori. Inoltre i miei sforzi non saranno stati di certo sufficienti a creare uno strumento
chiaro e completo. Per questo confido nella collaborazione dei lettori nel segnalarmi ogni
imprecisione che rileveranno e le parti poco chiare o incomplete.
In queste pagine sono presenti tutti gli argomenti che saranno trattati nei moduli didattici. I testi originali dai quali ho tratto ispirazione per la redazione di queste note sono
raccolti nella bibliografia, e pertanto invito tutti gli studenti a tenerli in considerazione
come riferimento ufficiale in caso di dubbi o imprecisioni.
Gli argomenti trattati in queste dispense seguono fedelmente quelli svolti in aula. In
dettaglio, nel Capitolo 1 verranno introdotti i concetti e le definizioni di base della teoria
dei grafi che possono essere approfonditi su [9], [2], [4], [7] e [14]; nel Capitolo 2
verrà introdotto il concetto di algoritmo e vedremo i metodi di misura della loro efficienza,
argomenti che possono essere studiati su [8], [11] e [12]. I testi [10], [2], [7] e [14]
possono essere usati come riferimento per il Capitolo 3 nel quale verranno illustrate alcune
importanti caratteristiche dei grafi come l’isomorfismo e l’eulerianità, mentre [14], [7],
[11] sono stati usati per il Capitolo 4 dove tratteremo i grafi planari. I testi [10], [7]
e [3] sono invece alla base dei Capitoli 5 e 6 dove verranno illustrati rispettivamente gli
algoritmi di ricerca su grafo e gli algoritmi di flusso.
Ottobre 2006
Capitolo 1
Introduzione
Ognuno di noi su questo pianeta è separato dagli altri solo da sei persone.
Sei gradi di separazione fra noi e tutti gli altri su questo pianeta: il presidente
degli Stati Uniti, un gondoliere veneziano... Non solo nomi importanti, chiunque:
un indigeno australiano, uno della Terra del Fuoco, un eschimese.
Io sono legato a chiunque sul pianeta da una catena di sei persone.
E’ un pensiero profondo... ognuno di noi è una porta spalancata su altri mondi.
John Guare
Per definire il concetto di grafo si fa spesso ricorso ad un episodio che viene associato
alla nascita stessa della Teoria dei Grafi, ovvero alla formulazione del famoso Problema dei
Ponti di Könisberg da parte di Eulero in un suo lavoro del 1736.
Nella città di Könisberg (all’epoca appartenente alla Prussia, attualmente situata in
Russia con il nome di Kaliningrad) ci sono, oggi come allora, delle aree abitative insediate
su alcune isole del fiume Pregel ed altre che sorgono lungo le sue rive, per un totale di
quattro aree. Queste zone sono unite tra di loro da sette ponti, cosı̀ come si può vedere
nell’attuale mappa della zona, riportata in Figura 1.1. Il problema posto da Eulero si può
formulare nel seguente modo: è possibile trovare un percorso chiuso che, a partire da una
qualunque area, permetta di attraversare i ponti una ed una sola volta e ritornare al punto
di partenza?
La zona presa in esame può essere schematizzata come in Figura 1.2 dove i punti in
grassetto rappresentano le aree (le due isole, indicate con le lettere a e d e le due rive
1
2
Introduzione
Figura 1.1: L’attuale mappa dell’area dei ponti del problema di Könisberg [1].
del fiume, indicate con le lettere b e c) e gli archi rappresentano i ponti tra le diverse
aree. Cercando di risolvere il problema ci si convince abbastanza presto che questo non
ha soluzione e che non ha soluzione anche se vogliamo passare per ogni ponte una ed una
sola volta, ma accettiamo di non ritornare esattamente nella stessa area. Eulero dimostrò
che la forma delle suddette aree non ha alcuna influenza, ma che la soluzione dipende solo
dalle proprietà di connessione. La soluzione a questo problema sarà data nel Capitolo 3.
b
a5
a1
a4
a3
a
d
a6
a2
a7
c
Figura 1.2: Grafo corrispondente al Problema di Könisberg.
Introduzione
1.1
3
Definizioni
In questa sezione verranno esposte le definizioni di base della Teoria dei Grafi. Il lettore
deve però essere allertato del fatto che non esiste una notazione universalmente accettata,
ma che spesso per gli stessi concetti possono trovarsi in letteratura notevoli differenze1 .
Per quanto possibile, verrà data segnalazione lungo le pagine di queste note.
Definizione 1.1.1 Un grafo G è una tripla costituita da un insieme di vertici V (G) (detti anche nodi), un insieme di archi E(G) e una relazione, detta relazione di incidenza,
che associa con un arco una coppia di vertici, detti estremi.
Un grafo può essere rappresentato graficamente mediante dei punti per indicare i nodi
e da linee e curve per gli archi.
Esempio 1.1.1 In Figura 1.2 è disegnato il grafo G rappresentante il Problema di Könisberg, dove l’insieme dei vertici è V (G) = {a, b, c, d} (le quattro regioni) e l’insieme degli
archi è E(G) = {a1 , a2 , a3 , a4 , a5 , a6 , a7 } (i ponti). L’insieme degli archi può rappresentarsi
anche come E(G) = {ab, ac, ad, bd, db, cd, dc} ed in questo modo risulta esplicitata maggiormente la relazione di incidenza. Notare che i nodi b e d (e anche c e d) hanno più di
un arco in comune.
Gli esempi di problemi che possono essere descritti come un grafo sono molteplici. Per
esempio è possibile descrivere la rete stradale come un grafo dove i nodi sono gli incroci e
gli archi sono le strade che li congiungono; analogamente, un circuito elettrico può essere
visto come un grafo i cui nodi sono le connessioni e gli archi i componenti del circuito
stesso.
Definizione 1.1.2 Un loop (o laccio) è un arco i cui estremi coincidono. Si chiamano
archi multipli gli archi i cui estremi sono gli stessi.
Definizione 1.1.3 Si definisce grafo semplice un grafo privo di loop ed archi multipli.
1
Nel luglio del 2006, Douglas West ha proposto un sondaggio su Internet rivolto alla comunità di coloro
che si occupano di Teoria dei Grafi per cercare di uniformare almeno alcune notazioni principali. Per
quanto possibile, l’esito di tale sondaggio è stato preso in considerazione in queste note
Introduzione
4
Individueremo un grafo semplice mediante i suoi insiemi di nodi e di archi, considerando
l’insieme degli archi come un insieme di coppie non ordinate di vertici e scrivendo e = ab
(o egualmente e = ba) per un arco con estremi a e b.
Si possono dare ulteriori definizioni sui nodi e sugli archi:
Definizione 1.1.4 Se ab ∈ E(G) allora a e b si dicono adiacenti e si indica con a ∼ b.
I vertici a e b si dicono incidenti sull’arco ab.
Due archi si dicono adiacenti se hanno un vertice in comune.
Definizione 1.1.5 Un grafo G si dice finito se gli insiemi V (G) ed E(G) sono finiti.
In gran parte delle applicazioni i grafi sono finiti e privi di loop ed archi multipli. Di
conseguenza, in seguito faremo riferimento quasi esclusivamente a grafi semplici e sempre
a grafi finiti. Per evitare confusione, inoltre, non considereremo il grafo nullo, ovvero il
grafo con l’insieme dei vertici e degli archi vuoti.
Per semplicità di notazione, nel seguito indicheremo con V o N l’insieme dei vertici
V (G) e con E l’insieme degli archi E(G).
1.1.1
Sottografi, ordine, dimensione e densità di un grafo
Definizione 1.1.6 Dato un grafo G = (V, E), G′ = (V ′ , E ′ ) è un sottografo di G se
V ′ ⊂ V ed E ′ ⊂ E e posso scrivere G′ ⊂ G. Se il sottografo G′ contiene tutti gli archi di G
che congiungono due vertici in G′ , allora G′ è chiamato sottografo indotto e si indica
con G[V ′ ]. Se il sottografo G′ ha V ′ ≡ V , allora G′ è chiamato sottografo ricoprente.
Per indicare un sottografo scriveremo genericamente G′ ⊆ G e diremo che G contiene G′ .
Esempio 1.1.2 Si consideri il grafo G = ({1, 2, 3, 4, 5, 6}, {12, 14, 16, 25, 34, 36, 45, 56}),
rappresentato in Figura 1.3. In Figura 1.4 è rappresentato un suo generico sottografo con G′ = ({1, 2, 3, 4, 6}, {12, 14, 16, 36}), in Figura 1.5 il sottografo indotto G′ =
({1, 2, 3, 4, 6}, {12, 14, 16, 34, 36}) ed infine in Figura 1.6 il sottografo ricoprente G′ =
({1, 2, 3, 4, 5, 6}, {12, 14, 16, 36, 56}).
5
Introduzione
1
1
2
6
2
3
5
3
6
4
4
Figura 1.3: Grafo dell’Esempio 1.1.2
1
Figura 1.4: Un sottografo
1
2
6
3
2
6
3
5
4
4
Figura 1.5: Un sottografo indotto
Figura 1.6: Un sottografo ricoprente
Spesso è possibile costruire nuovi grafi a partire da un grafo assegnato G cancellando
oppure aggiungendo vertici ed archi. Se V ′ ⊂ V , allora G − V ′ = G[V \V ′ ] è il sottografo di
G ottenuto cancellando tutti i vertici V ′ e tutti gli archi su di loro incidenti. Analogamente,
se E ′ ⊂ E, allora G − E ′ = (V, E\E ′ ). Se V ′ = {v} oppure E ′ = {xy}, allora la notazione
si semplifica in G − v e G − xy. Infine, se x ed y sono due vertici non adiacenti di G, allora
G + xy è ottenuto congiungendo x ed y con un arco.
Definizione 1.1.7 Si definisce ordine di un grafo G il numero dei suoi vertici, ovvero
|G| = |V (G)|; si definisce dimensione di un grafo il numero dei sui archi, ovvero e(G) =
|E(G)|.
6
Introduzione
Per indicare l’ordine di un grafo arbitrario si può utilizzare inoltre la notazione Gn mentre attraverso la notazione G(n, m) si indica un grafo arbitrario di ordine n ed dimensione
m. Nell’esempio 1.1.2 il grafo ha ordine 6 e dimensione 8 e potremo indicarlo per esempio
come G(6, 8).
Dato un grafo semplice arbitrario di ordine n, ci chiediamo in quale campo di valori
possa variare il numero di archi m. Chiaramente, tali valori possono variare tra 0 (in
corrispondenza del grafo nullo) e tra il numero corrispondente al caso in cui tutti i nodi
sono mutualmente adiacenti, numero pari a tutte le possibili combinazioni di n oggetti
presi due a due. Quindi:
n(n − 1)
n
0≤m≤
=
2
2
(1.1)
Nel caso specifico il coefficiente binomiale si scrive attraverso la formula che compare
nell’ultimo membro della Formula 1.1. Quindi un grafo d’ordine 6 potrà, per esempio,
avere dimensione compresa tra 0 e 15.
Spesso il numero di archi di un grafo viene espresso in modo proporzionale rispetto al
massimo numero di archi possibile. A tale misura viene data il nome di densità di un
grafo e si calcola come:
D(G) =
m
n(n − 1)/2
(1.2)
chiaramente, tale misura varia da 0 a 1. In generale, diremo sparso un grafo che abbia
un numero di archi molto basso, ovvero una densità bassa (per esempio D(G) ≈ 0, 1÷0, 3);
viceversa, diremo che é denso un grafo che ha un alto numero di archi, quindi una densità
alta (per esempio D(G) ≈ 0, 7 ÷ 0, 9).
I grafi che hanno tutti i nodi mutualmente adiacenti rivestono una importanza particolare e sono chiamati grafi completi (o clique) e sono indicati con Kn , dove n indica la
sua cardinalità. Dato che un grafo può contenere sottografi completi, possiamo dare la
definizione più generale:
Definizione 1.1.8 Un grafo completo (o clique) in un grafo G è un insieme di nodi
7
Introduzione
mutualmente adiacenti. Dato l’insieme di tutte le clique di G, la cardinalità della clique
massima viene chiamata numero di clique ed è indicato con ω(G).
1.1.2
Vicinati, gradi e regolarità
Passiamo ora a dare altre definizioni sui grafi.
Definizione 1.1.9 L’insieme dei vertici adiacenti ad un assegnato vertice x ∈ G è
chiamato vicinato di x e si indica con Γ(x).
Con il simbolo Γ(x) spesso ci si riferisce al vicinato aperto, mentre per indicare il vicinato
chiuso si usa scrivere Γ(x) ∪ {x}. Notare che se y ∈ Γ(x) e x ∈ Γ(y), allora significa che
x ∼ y e y ∼ x, ovvero xy è un arco del grafo.
Definizione 1.1.10 Si definisce grado d(x) di un nodo x ∈ G la cardinalità del suo
vicinato, ovvero, d(x) = |Γ(x)|. Se un vertice ha d(x) = 0, allora si definisce vertice
isolato.
Anche in questo caso il simbolismo non è univocamente definito, perchè possiamo trovare indicato il vicinato ed il grado rispettivamente con ΓG (x) e dG (x), in modo da non
sottintendere il grafo G.
Dato un grafo G, si usa indicare il suo grado minimo con δ(G) ed il suo grado massimo
con ∆(G)
L’introduzione del concetto di grado di un vertice ci permette di introdurre un primo
importante risultato generale, che viene chiamato spesso Lemma Handshaking 2 :
Lemma 1.1.1 In ogni grafo, il numero di vertici di grado dispari è pari.
Dimostrazione: Sommando i gradi di ogni vertice, ogni arco è contato esattamente due
volte (una volta per ogni vertice). Quindi avremo che:
X
d(i) = 2m
(1.3)
∀i∈V
2
La ragione del nome del lemma deriva dal fatto che in un ricevimento, se tutti gli invitati si salutano
tra loro allora la somma totale delle strette di mano (in inglese, handshaking) è sempre pari.
8
Introduzione
Dato che il secondo membro è positivo, anche il primo deve esserlo; quest’ultimo è
composto da due contributi, la somma dei gradi pari e la somma dei gradi dispari. È
quindi chiaro che il contributo dei vertici di grado dispari deve essere pari e questo accade
solo se sono in numero pari.
Definizione 1.1.11 Dato un grafo G, se δ(G) = ∆(g) = k allora G si dice k-regolare.
Un grafo G si dice regolare se è k-regolare per un certo k.
Figura 1.7: Esempi di grafi 3-regolari
Per esempio, tutte le clique Kn sono (n − 1)-regolari; ancora, se k = 3 si ha un grafo
3-regolare (vedi Figura 1.7), detto anche cubico.
1.1.3
k-Fattorizzazioni
Definizione 1.1.12 Un k-fattore è un sottografo ricoprente k-regolare.
Se l’insieme degli archi può essere diviso in k-fattori ho una decomposizione che prende
il nome di k-fattorizzazione. Se ho una decomposizione in 1-fattori allora la decomposizione prende il nome di fattorizzazione ed ha ovviamente senso solo se il grafo ha un
numero pari di nodi. In Figura 1.8 sono rappresentate (con gli archi in grassetto) le tre
possibili fattorizzazioni di K4 .
9
Introduzione
1
2
1
2
1
2
3
4
3
4
3
4
Figura 1.8: Le fattorizzazioni di K4
1.2
Grafi e modelli
In questa sezione verranno mostrate alcune applicazioni pratiche che possono essere rappresentate mediante dei grafi. Gli esempi illustrati ci serviranno soprattutto per introdurre
nuove definizioni e termini della teoria dei grafi.
1.2.1
Relazioni e sottografi
Supponiamo sia assegnato un gruppo di cinque persone; come è possibile rappresentare
le relazioni di conoscenza tra i membri del gruppo? Se indichiamo con le prime cinque
lettere dell’alfabeto i cinque membri del gruppo, possiamo costruire un grafo nel quale i
nodi rappresentano i membri, ed un arco esiste se due membri si conoscono. Supponiamo
sia assegnata un’istanza di tale problema come in Figura 1.9.
Supponiamo di voler studiare ora il problema complementare: dato ancora lo stesso
gruppo di persone, come rappresentare le relazioni di non conoscenza tra i membri?
Definizione 1.2.1 Il grafo complemento Ḡ di un grafo semplice G è un grafo semplice
avente insieme dei nodi V (Ḡ) ≡ V (G) ed insieme degli archi tale che un arco ab esiste in
Ḡ se e solo se ab ∈
/ E(G).
Definizione 1.2.2 Un insieme indipendente (o insieme stabile) W in un grafo è un
insieme di vertici mutuamente non adiacenti, ovvero se e soltanto se il sottografo indotto
G[W ] = ∅.
10
Introduzione
b
b
a
c
e
a
d
Figura 1.9: Grafo rappresentante le
relazioni di conoscenza tra i cinque
membri del gruppo
c
e
d
Figura 1.10: Grafo rappresentante le
relazioni di non conoscenza tra i cinque
membri del gruppo
La definizione di complementarietà ci permette di legare tra di loro i due problemi
introdotti e di ottenere il grafo rappresentante il secondo attraverso tale definizione, come
si può vedere in Figura 1.10. Possiamo notare inoltre che nel grafo G, il sottografo {a, b, c}
è una clique di ordine 3 e {c, e} è un insieme indipendente di cardinalità 2. Corrispondentemente, nel grafo G, i nodi {a, b, c} sono un insieme indipendente di cardinalità 3 e {c, e}
è una clique di ordine 2. Quindi gli insiemi indipendenti di un grafo diventano clique nel
suo grafo complementare cosı̀ come le clique in un grafo diventano insiemi indipendenti nel
suo grafo complemento.
L’insieme indipendente, oltre che alla clique, è legato ad un altro concetto, ovvero quello
del vertex covering:
Definizione 1.2.3 Un vertex covering di un grafo G è un sottoinsieme V ′ di V tale
che ogni arco di G è incidente con almeno un vertice in V ′ .
Si può dimostrare [14] che, se α è il valore della cardinalità del massimo insieme indipendente di G e β la cardinalità del più piccolo V ′ tale che sia un vertex covering di G,
allora α + β = n, dove n è l’ordine di G. Infatti, ogni insieme indipendente di cardinalità
massima è il complemento di un vertex cover minimo.
1.2.2
Assegnamento di lavori
Si supponga che un’azienda debba effettuare n compiti diversi, da assegnare ognuno ad
m dipendenti, che però non sono interscambiabili perchè ognuno di essi possiede delle
11
Introduzione
specializzazioni. In che modo assegnare i compiti ai dipendenti?
Per risolvere questo problema possiamo costruire un grafo nel quale i vertici sono gli
n compiti e gli m dipendenti, mentre un arco ij esiste se il compito i può essere svolto
dal dipendente j. In Figura 1.11 è disegnato un grafo corrispondente al caso in cui n =
m = 4 e si può notare come in realtà l’insieme dei nodi sia partizionato in due sottoinsiemi
(indipendenti) V1 (corrispondenti ai compiti) e V2 (corrispondenti ai dipendenti).
V1
V2
Figura 1.11: Grafo bipartito
Per questa particolare categoria di grafi possiamo dare la seguente definizione:
Definizione 1.2.4 Un grafo G si dice bipartito se l’insieme dei suoi nodi V (G) è l’unione
di due insiemi indipendenti disgiunti V1 e V2 chiamati insiemi partizione ed ogni suo arco
va da V1 a V2 .
Un grafo bipartito è anche chiamato 2-partito, perchè può essere visto come un particolare caso dei grafi r-partiti, ovvero di quei grafi nei quali l’insieme dei nodi è costituito
dall’unione di r insiemi partizione e gli archi vanno da una delle partizioni verso una
qualunque delle altre.
1.2.3
Scheduling di team in una azienda
Una azienda ha la necessità di schedulare3 un certo numero di riunioni dei vari team, ma
ha il vincolo che alcuni manager appartengono a team differenti e quindi non è possibile
indire due riunioni contemporaneamente se hanno un manager in comune.
3
Il termine inglese scheduling indica un problema decisionale che considera l’allocazione di risorse scarse ad attività, con l’obiettivo di ottimizzare una o più misure di prestazione. Siccome tale parola è
intraducibile in italiano, viene di solito adattata, ottenendo i termini schedulare, schedulato, ecc.
12
Introduzione
1
2
1
a
b
e
1
3
c
d
Figura 1.12: Una istanza del problema dei team
Per rappresentare tale problema posso disegnare un grafo nel quale i nodi sono i team,
ed esiste un arco se i due team hanno un membro in comune, come in Figura 1.12. Per
risolverlo, invece, posso usare un algoritmo che, partendo da un nodo, assegni ad ognuno
di essi una etichetta (colore) in modo che due nodi adiacenti non abbiano stessa etichetta. Associando l’etichetta ad un istante temporale, e considerando che nodi che hanno
stessa etichetta non hanno membri in comune, ne consegue che per risolvere il problema
è sufficiente schedulare i team nell’ordine dato dalle etichette. Per esempio, considerando
le etichette assegnate al grafo in Figura 1.12 una possibile sequenza è {a, c, e}, {b} e {d}
(notare come l’ordine delle etichette corrisponda agli istanti di attuazione).
Generalizzando il problema, ci si può chiedere quale sia, tra tutte le etichettature
ammissibili, quella che implica il minor numero di etichette possibili.
Definizione 1.2.5 Si definisce numero cromatico di un grafo G e si indica con χ(G),
il minimo numero di colori necessari per etichettare i vertici in modo che vertici adiacenti
ricevano colori diversi.
Il termine colore deriva dal famoso Problema dei quattro colori, posto da Francis e
Frederick Guthrie nel 1852 e da A. Cayley nel 1878, nel quale si ci si chiedeva quale fosse il
minimo numero di colori necessari per colorare una mappa geografica politica. Nel 1890 P.
J. Heawood congetturò che bastassero quattro colori, ma tale congettura è stata dimostrata
definitivamente solo nel 1976 da K. Appel e W. Haken. Per altri aspetti storici sul problema
dei quattro colori rimandiamo il lettore a [9].
Introduzione
1.2.4
13
Percorsi su rete
Uno dei campi nel quale la teoria dei grafi trova grande applicazione è quello della rappresentazione delle reti stradali. Infatti, possiamo associare un vertice del grafo ad ogni
incrocio stradale ed unire tra di loro tali vertici con un arco in corrispondenza del tratto di
strada che unisce i due incroci. Inoltre possiamo iniziare ad introdurre dei nuovi elementi
che possiamo associare agli archi ed ai nodi, ovvero dei pesi; in questo caso, un peso associato agli archi può rappresentare la lunghezza del tratto stradale, oppure la sua capacità
in termini di auto per ora, ecc.
Questa rappresentazione fa nascere alcuni quesiti, per esempio, dati due vertici qualunque a e b, è possibile individuare sulla rete il cammino più breve tra questi due punti?
Oppure, se i vertici rappresentano delle città e gli archi la rete autostradale, è possibile
individuare un percorso che partendo da una generica città visiti tutte le restanti con un
percorso di distanza minima?
Vedremo nel seguito come risolvere il primo problema (vedi Capitolo 6) mentre per il
secondo rimandiamo alla bibliografia, invitando il lettore ad approfondire gli argomenti
riguardanti il cosiddetto Problema del commesso viaggiatore o TSP, Travelling Salesman
Problem per esempio in [7] o [14].
Definizione 1.2.6 Un cammino (o anche path) è un grafo semplice i cui vertici possono
essere ordinati in una lista in modo che due vertici sono consecutivi se e soltanto se sono
consecutivi nella lista medesima.
Quindi, P = (V (P ) = {x0 , x1 , . . . xl }, E(P ) = {x0 x1 , x1 x2 . . . xl−1 xl }) è un cammino
che indicheremo con x0 x1 . . . xl . Chiameremo i vertici x0 e xl terminali del cammino.
P
Inoltre, se indichiamo con di la lunghezza associata ad ogni arco, allora l = ∀xi xj ∈E(P ) di
è la lunghezza del cammino.
Definizione 1.2.7 Un ciclo è un cammino con egual numero di vertici ed archi e con i
terminali coincidenti.
A titolo di esempio, in Figura 1.13 è rappresentato il cammino x0 x1 x3 x2 x4 x5 , mentre
in Figura 1.14 è rappresentato il ciclo x0 x1 x3 x2 x4 x5 x0 .
14
Introduzione
x2
x1
x0
x2
x1
x0
x3
x4
x5
x3
x4
x5
Figura 1.13: Un cammino
Figura 1.14: Un ciclo
P3
C4
P4
C5
Figura 1.15: Alcuni esempi di path e cicli
Per indicare path e cicli arbitrari si usa aggiungere alla nomenclatura un pedice che
indica il numero di archi di cui sono composti. Quindi, per esempio, P4 indica un path
di lunghezza 4, mentre C5 indica un ciclo di lunghezza 5 (alcuni esempi sono dati in
Figura 1.15). Inoltre, un path con terminali x e y viene spesso indicato come “{x, y}-path”.
Un cammino od un ciclo possono essere identificati come sottografi in un grafo arbitrario
e questo ci porta a generalizzare le definizioni appena date.
Definizione 1.2.8 Dato un grafo arbitrario G = (V, E), allora:
• un walk W in un grafo è una sequenza alternata di vertici ed archi, x0 e1 x1 e2 . . . el xl ,
con ei = xi−1 xi e 0 < i ≤ l;
• un trail T è un walk con tutti gli archi distinti;
• un path P è un trail con tutti i vertici distinti;
• un trail chiuso (o anche circuito) è un trail con x0 ≡ xl ;
15
Introduzione
• un ciclo C è un walk composto da più di tre archi, ha x0 ≡ xl ed ogni vertice distinto.
Definizione 1.2.9 Dato un grafo G, un insieme di cammini {P 1 , P 2 , . . . P k } si dice indipendente (o internamente disgiunto) se per ogni coppia di cammini, gli unici vertici
in comune sono i terminali, ovvero sono dei {x, y}-path se V (P i )∩V (P j ) = {x, y}, ∀i 6= j.
Esempio 1.2.1 Dato il grafo in Figura 1.16, un walk è {abcebc}, un trail è {abcebf }, un
trail chiuso è {abcebf ea}, un path è {abcdef } ed, infine, un ciclo è {abcdef a}.
b
a
c
d
f
e
Figura 1.16: Grafo per l’Esempio 1.2.1
Il concetto di cammino in un grafo ci permette di introdurre un’importante proprietà
dei grafi, ovvero la connessione.
Definizione 1.2.10 Un grafo G è connesso se ogni coppia di suoi vertici appartiene ad
un cammino e sconnesso altrimenti.
Riprenderemo questo concetto nelle prossime sezioni.
1.3
Esercizi
Es. 1.3.1 Dire se è possibile disegnare un grafo in cui i gradi dei vertici siano i seguenti:
d(1) = 1; d(2) = 2; d(3) = 3; d(4) = 4; d(5) = 5.
Introduzione
16
Es. 1.3.2 E’ possibile disegnare un grafo 3-regolare con 9 vertici? Se si disegnarlo. E con
8 vertici? Se si disegnarlo.
Es. 1.3.3 Dato K6 , quanti cammini ricoprenti posso fare partendo da un vertice assegnato
a?
Es. 1.3.4 Dato K4,4 , quanti cammini ricoprenti posso fare partendo da un dato vertice a?
Es. 1.3.5 Si disegni una 2-fattorizzazione di K5 .
Es. 1.3.6 Si mostri se è possibile disegnare una 2-fattorizzazione di K6 .
Es. 1.3.7 (Difficile) Determinare il numero di 1-fattori in K2n .
Es. 1.3.8 (Difficile) Determinare il numero di 2-fattori in K2n,2n .
Capitolo 2
Algoritmi
I progressi ottenuti nel campo dell’elaborazione elettronica hanno permesso lo sviluppo e
l’applicazione dei metodi matematici per la modellizzazione e la risoluzione di una grande varietà di problemi decisionali, anche di dimensioni ragguardevoli. Tutto l’insieme di
metodologie che hanno in comune l’uso del metodo matematico, come per esempio l’ottimizzazione, la programmazione matematica, la teoria dei grafi, la teoria delle code, la
teoria delle decisioni, la simulazione, ecc, sono raccolte in una disciplina che prende il nome
di Ricerca Operativa. Data la natura applicativa della Ricerca Operativa, lo studio teorico
del problema matematico posto viene normalmente affiancato allo studio delle tecniche
necessarie per ottenere una soluzione in modo efficiente.
2.1
Breve tassonomia dei problemi decisionali
In generale, nella modellazione di un problema decisionale, ci dobbiamo preoccupare di
tre componenti fondamentali: il grado di incertezza, il numero di obiettivi ed il numero di
decisori.
Il grado di incertezza indica se ci si trova in condizioni di informazione completa, come
nei problemi deterministici, oppure in condizioni di conoscenza parziale, come nei problemi stocastici. Il numero di obiettivi è un’altra componente da conoscere e, in generale,
potremo riconoscere problemi a singolo obiettivo, oppure multiobiettivo. Analogamente,
17
Algoritmi
18
per il numero dei decisori avremo una divisione in due classi, quella dei problemi a singolo
decisore e quella dei problemi multidecisore.
Continuando con questi accenni di tassonomia dei problemi decisionali, potremo considerare problemi lineari oppure problemi non lineari, a seconda della nostra funzione
obiettivo. Inoltre, sulla base dei valori che possono assumere le variabili, avremo problemi
continui se i valori apparterranno allo spazio dei numeri reali R, problemi discreti (o combinatori) se i valori apparterranno allo spazio dei numeri interi Z ed infine, problemi misti
se le variabili possono assumere valori sia reali che interi.
Utilizzando lo schema appena descritto, nella trattazione della teoria dei grafi ci limiteremo in seguito al solo studio dei problemi deterministici, singolo obiettivo, singolo
decisore, lineari e discreti. Al lettore è lasciato immaginare la quantità di problemi decisionali che si possono osservare nella realtà al presentarsi ed al combinarsi delle diverse
ipotesi sopra dette e, di conseguenza, come la loro analisi sia fondamentale per individuare
le tecniche più adeguate per risolverli.
2.2
Algoritmi
Per la soluzione di un problema, occorre individuare un metodo generale (procedura) in
grado di risolvere ogni generica istanza, fornendoci la soluzione desiderata in un certo
numero di passi. Il termine generale usato per definire tali procedure è algoritmo 1 .
Definizione 2.2.1 Un algoritmo è una procedura definita usata per risolvere un problema
usando un numero finito di passi.
Gli algoritmi ricopriranno una grande importanza nel nostro studio della Teoria dei
Grafi e, quindi, dedicheremo le prossime pagine alla descrizione di alcuni strumenti per la
loro analisi.
Esempio 2.2.1 Descrivere un algoritmo per trovare l’elemento più grande in una sequenza
(lista) di interi.
1
Il termine algoritmo deriva dal nome del matematico persiano Abu Ja’far Mohammed ibn Musa alKhowarizmi, vissuto nel IX secolo d.C..
19
Algoritmi
Per specificare la procedura di risoluzione di questo semplice problema possiamo utilizzare
molti metodi, ma uno dei più semplici è quello di utilizzare il linguaggio naturale per
descrivere i singoli passi della procedura. Per risolvere il problema devono essere eseguiti
i seguenti passi:
Step 1: Poni il massimo temporaneo uguale al primo intero della sequenza;
Step 2: Compara il prossimo intero nella sequenza.
Se è più grande, poni il massimo
temporaneo pari a tale valore;
Step 3: Ripetere il passo precedente per ogni altro elemento della lista;
Step 4: Stop se non ci sono altri interi. Il valore cercato è contenuto nel massimo temporaneo
(che diventa definitivo).
Per descrivere più efficacemente un algoritmo si può utilizzare una descrizione mediante pseudocodice, basata su una sintassi molto simile al linguaggio di programmazione PASCAL, di facile comprensione per chiunque abbia dei rudimenti di Fondamenti di Informatica. Essa inoltre ci permette di evitare le specificità di un linguaggio di
programmazione.
Utilizzando lo pseudocodice, il nostro algoritmo diventa:
procedure MAX(a1 , . . . , an ; integers)
max := a1
for i := 2 to n
if max < ai then max := ai
max contiene il massimo
Come è facile notare, l’algoritmo in pseudocodice segue fedelmente i passi sopra descritti.
Per poter fornire una soluzione significativa, gli algoritmi devono rispettare alcune
proprietà:
Proprietà 2.2.1 Un algoritmo deve soddisfare le seguenti proprietà:
20
Algoritmi
• Input - L’algoritmo deve avere un input contenuto in un insieme definito I.
• Output - Da ogni insieme di valori in input, l’algoritmo produce un insieme di valori
in uscita che comprende la soluzione.
• Determinatezza - I passi dell’algoritmo devono essere definiti precisamente.
• Finitezza - Un algoritmo deve produrre la soluzione in un numero di passi finito
(eventualmente molto grande) per ogni possibile input definito su I.
• Efficacia - Deve essere possibile effettuare ogni passo dell’algoritmo esattamente ed
in un tempo finito.
• Generalità - L’algoritmo deve essere valido per ogni insieme di dati contenuti in I
e non solo per alcuni.
Oltre a queste proprietà, un algoritmo deve essere efficiente, ovvero, dato un input di
dimensione fissata, deve fornire una soluzione in un tempo ragionevole ed inoltre deve occupare una quantità limitata di memoria di un computer. Problematiche di questo tipo sono
trattate dall’analisi della complessità computazionale degli algoritmi; in particolare,
se l’oggetto dello studio è il tempo di elaborazione, parleremo di complessità temporale, mentre se l’oggetto è l’occupazione della memoria, allora parleremo di complessità
spaziale.
È chiaro quindi che nell’analisi di un algoritmo è di fondamentale importanza sapere se
risolverà il nostro problema in un microsecondo, in un ora o in un secolo. Analogamente,
è importante sapere se l’occupazione di memoria possa eccedere le capacità disponibili.
L’analisi della complessità spaziale coinvolge principalmente l’analisi delle strutture dati
e, quindi, esula dagli scopi di queste note. Viceversa, l’analisi della complessità temporale
è molto importante per gli algoritmi su grafo e sarà approfondita nella Sezione 2.4.
2.2.1
Algoritmi di ricerca
In questa sezione vedremo alcuni esempi di algoritmi ed in particolare ci concentriamo sugli
algoritmi di ricerca su stringa che abbiamo già introdotto nell’Esempio 2.2.1.
Algoritmi
21
Gli algoritmi di ricerca rivestono un particolare interesse nella pratica; basti pensare
alla necessità di trovare una parola in un dizionario, un dato in un database o anche alla
ricerca di pagine web attraverso i motori di ricerca. In quest’ultimo caso, l’algoritmo di
ricerca presenta, ovviamente, complessità ben diverse.
Il primo algoritmo che introduciamo è l’algoritmo di ricerca lineare (o sequenziale): data una lista di elementi distinti a1 , a2 . . . , an , localizzare l’elemento x o affermare
che non c’è.
procedure LINEAR SEARCH (x, integer; a1 , . . . , an , distinct integers)
i := 1
while (i ≤ n AND x 6= ai )
i := i + 1
if i ≤ n then
posizione := i
else
posizione := 0
L’algoritmo inizia confrontando x con a1 . Se l’elemento non è stato individuato, si
incrementa il contatore i e quindi si continua fino a che una delle due condizioni risulta
falsa (cioè o sono arrivato alla fine della lista, o ho trovato l’elemento) ed il ciclo while
termina. L’istruzione condizionale if ha il compito di inserire nella variabile di output il
valore della posizione o il valore 0 se tale valore non è nella lista.
Il secondo algoritmo di ricerca che descriviamo è l’algoritmo di ricerca binaria:
data una lista di elementi distinti a1 , a2 . . . , an , ordinati in modo che a1 ≤ a2 ≤ . . . ≤ an ,
localizzare l’elemento x o affermare che non c’è.
La differenza in questo caso è che la sequenza è ordinata in modo crescente, come
per esempio può accadere in un vocabolario se il criterio adottato è quello lessicografico.
Supponiamo allora che sia assegnata la sequenza {1, 2, 4, 5, 6, 9, 10, 12, 15, 18, 20, 24} e che
si voglia trovare se il numero 18 appartiene a tale lista.
L’idea che sta alla base dell’algoritmo è quella di dividere ad ogni passo la lista in due
parti (nel nostro caso {1, 2, 4, 5, 6, 9} e {10, 12, 15, 18, 20, 24}) e confrontare l’elemento da
cercare rispettivamente con l’ultimo elemento della prima metà e con il primo elemento
della seconda metà. Nel nostro caso l’elemento è più grande del primo elemento della
Algoritmi
22
seconda metà e quindi possiamo concentrarci solo in tale stringa per la ricerca. L’algoritmo
continua dividendo tale stringa ulteriormente, ottenendo pertanto {10, 12, 15} e {18, 20, 24}
ed eseguendo di nuovo i confronti. Cosı̀ facendo l’algoritmo genera ancora le stringhe {18}
e {20, 24} arrestandosi al valore cercato oppure affermando che non appartiene a tale lista.
Lo pseudocodice dell’algoritmo proposto è il seguente:
procedure BINARY SEARCH (x, integer; a1 , . . . , an , increasing integers)
i := 1
j := n
while (i < j )
begin
⌋
m := ⌊ i+j
2
if x > am then
i := m + 1
else
j := m
end
if x = ai then
posizione := i
else
posizione := 0
L’apparente complessità dell’algoritmo di ricerca binaria, rispetto a quello lineare,
nasconde dei benefici che saranno mostrati nel paragrafo 2.4.
2.3
Crescita di funzioni
Nell’analisi di un algoritmo è di particolare interesse comprendere la sua applicabilità pratica e, principalmente, capire il tempo necessario per ottenere un risultato utile, ovvero la
sua efficienza. Osservando gli algoritmi presentati nella sezione precedente, si può notare
come l’input sia sempre legato al numero n di oggetti in ingresso sui quali eseguire l’elaborazione. Ci potremmo quindi chiedere quanto cresce il tempo di elaborazione al crescere di
n e se è possibile trovare una funzione f (n) che sia in grado di trasferire questa informazione. Inoltre, sarebbe utile disporre di un criterio in grado di paragonare due algoritmi
confrontando la crescita delle rispettive funzioni dell’input.
23
Algoritmi
2.3.1
Notazione Big-O
Per analizzare il comportamento degli algoritmi dobbiamo prima introdurre la notazione
Big-O, necessaria per lo studio della crescita di una funzione generica, di cui segue la
definizione:
Definizione 2.3.1 Siano f e g due funzioni tali che f, g : N → R (o anche f, g : R → R).
Diremo che f (n) = O(g(n)) se esistono due costanti, C e k, tali che ∀n > k si ha:
f (n) ≤ C|g(n)|
(2.1)
La 2.1 si legge come “f (n) è un big o di g(n)”.
È importante notare che basta trovare una sola coppia C, k tale che sia vera la 2.1. In
realtà, una coppia che soddisfa la definizione data non è mai unica, anzi, basta prendere
una qualunque coppia C ′ , k ′ tale che C < C ′ e k < k ′ per soddisfare la definizione e questo
ci porta a dire che se una coppia esiste, allora ne esistono infinite.
Esempio 2.3.1 Mostrare che f (n) = n2 + 2n + 1 è un O(n2 ).
Per risolvere questo esercizio basta osservare che 0 ≤ n2 + 2n + 1 ≤ n2 + 2n2 + n2 = 4n2 ,
avendo considerato C = 4 e k = 1. Notare che in questo caso si ha che f (n) ≤ C|g(n)| e
g(n) ≤ C|f (n)|. Quando ciò accade diremo che le funzioni sono dello stesso ordine.
Occorre notare che il segno di uguale nella 2.1 non è realmente un uguale, ma, piuttosto,
indica che in questa notazione, quando si hanno dei valori di n sufficientemente grandi nei
dominii di f e g, la disuguaglianza è verificata.
Se f (n) ≤ C|g(n)| e h(n) è una ulteriore funzione che assume valori assoluti maggiori
di g(n) a partire da valori di n sufficientemente grandi, allora si ha ovviamente che f (n) ≤
C|h(n)|. Di norma, però, si sceglie la funzione g più piccola possibile; quindi nell’esempio
precedente è corretto, ma privo di senso, dire che f (n) = O(n3 ).
Esempio 2.3.2 Dare una stima Big-O della somma dei primi numeri n interi positivi.
24
Algoritmi
Dato che 1 + 2 + . . . + n ≤ n + n + . . . + n = n2 , allora 1 + 2 + . . . + n = O(n2 ), con C = 1
e k = 1.
Esempio 2.3.3 Dare una stima Big-O di f (n) = log n!.
Per quanto riguarda il fattoriale si ha che 1 · 2 · . . . · n ≤ n · n · . . . · n = nn . Quindi
log n! ≤ log nn = n log n, che implica log n! = O(n log n).
2.3.2
Crescita di combinazioni di funzioni
Gli algoritmi sono tipicamente composti da diverse operazioni concatenate ed annidate in
sottoprocedure, quindi la notazione introdotta nel paragrafo precedente deve essere estesa
in modo da tenere conto del peso delle singole sottoprocedure.
Supponiamo allora di avere assegnate due funzioni f1 (n) = O(g1 (n)) e f2 (n) = O(g2 (n)).
Per la definizione data nel paragrafo precedente sappiamo che esistono delle costanti C1 ,
C2 , k1 e k2 tali che f1 (n) ≤ C1 |g1 (n)|, ∀n > k1 e f2 (n) ≤ C2 |g2 (n)|, ∀n > k2 .
Teorema 2.3.1 Si supponga che f1 (n) = O(g1 (n)) e f2 (n) = O(g2 (n)). Allora la somma
delle due funzioni è:
(f1 + f2 )(n) = O(max{g1 (n), g2 (n)})
(2.2)
Dimostrazione: Si noti che |(f1 +f2 )(n)| = |f1 (n)+f2 (n)| ≤ |f1 (n)|+|f2 (n)| (quest’ultima
relazione è vera per la disuguaglianza triangolare, |x + y| ≤ |x| + |y|).
Se si considera g(n) = max{g1 (n), g2 (n)} e C = C1 + C2 , allora |f1 (n)| + |f2 (n)| ≤
C1 |g1 (n)| + C2 |g2 (n)| ≤ C|g(n)|.
Corollario 2.3.2 Se entrambe le funzioni f1 (n) e f2 (n) sono entrambe O(g(n)), allora
(f1 + f2 )(n) = O(g(n)).
Per quanto riguarda il prodotto di funzioni, vale il seguente teorema:
25
Algoritmi
Teorema 2.3.3 Si supponga che f1 (n) = O(g1 (n)) e f2 (n) = O(g2 (n)). Allora il prodotto
delle due funzioni è:
(f1 · f2 )(n) = O(g1 (n) · g2 (n))
(2.3)
Dimostrazione: Considerando C = C1 · C2 , allora (f1 · f2 )(n) = |f1 (n)| · |f2 (n)| ≤
C1 |g1 (n)| · C2 |g2 (n)| ≤ C|(g1 · g2 )(n)|.
Esempio 2.3.4 Dare una stima Big-O della funzione f (n) = 3n log(n!) + (n2 + 3) log n.
Considerando ogni termine singolarmente abbiamo:
f (n) =
3n
→
n
log n!
→
n log n
→
log n
+(n2 + 3) → n2 + 3 ≤ 4n2
log n
Quindi f (n) = O(n2 log n).
L’ultimo risultato di questa sezione riguarda la crescita di funzioni polinomiali (in x,
per comodità di notazione):
Teorema 2.3.4 Sia f (x) = an xn + an−1 xn−1 + . . . + a0 , con an , an−1 . . . , a0 numeri reali.
Allora f (x) = O(xn ).
Dimostrazione: Utilizzando la disuguaglianza triangolare, e per x > 1,
|f (x)| = |an xn + an−1 xn−1 + . . . + a0 |
≤ |an |xn + |an−1 |xn−1 + . . . + |a0 |
= xn (|an | +
|a0 |
|an−1 |
+ ... + n )
x
x
≤ xn (|an | + |an−1 | + . . . + |a0 |)
Questo dimostra che |f (x)| < Cxn , dove C = |an | + |an−1 | + . . . + |a0 |, se x > 1. Quindi
f (x) = O(xn ).
26
Algoritmi
Come abbiamo detto, la notazione Big-O viene usata per la stima del numero di operazioni necessarie affinchè un algoritmo risolva un dato problema. Le funzioni che normalmente si usano sono 1, log n, n, n log n, n2 , 2n , n!. La sequenza presentata non è casuale, ma
rispetta l’ordinamento tale per cui la funzione successiva è sempre più grande di quella che
la precede. In Figura 2.1 sono riportati i grafici delle funzioni indicate, dove n sia l’ascisse
e in ordinata siano i valori della funzione, in scala esponenzialmente crescente.
2048
n!
1024
2n
512
256
128
n2
64
32
16
8
n log n
n
4
log n
2
1
1
2
3
4
5
6
7
8
Figura 2.1: Grafico delle funzioni più comunemente usate nella stima Big-O
2.4
Complessità degli algoritmi
L’analisi della complessità temporale degli algoritmi può essere espressa in termini di numero di operazioni eseguite dallo specifico algoritmo quando l’input ha un data dimensione.
Questo tipo di analisi risulta essere più efficiente della semplice misura del tempo impiegato da un computer per completare la sua elaborazione, perché, nel caso, la velocità di
elaborazione può variare molto da computer a computer ed è inoltre difficile da misurare
e valutare.
Algoritmi
27
Per illustrare come analizzare la complessità di un algoritmo, consideriamo il primo
esempio della Sezione 2.2 per trovare l’elemento più grande in una lista. Le operazioni
che sono eseguite sono i due confronti all’interno del ciclo for, uno per verificare se si è
giunti alla fine della lista, l’altro per aggiornare, eventualmente, il massimo temporaneo.
Dato che i due confronti vengono ripetuti dal passo due al passo n ed è poi eseguito
un’ulteriore confronto per uscire dal ciclo quando il contatore i = n + 1, si ha che sono
eseguiti esattamente 2(n − 1) + 1 = 2n − 1 confronti. Quindi, dato un input di lunghezza
n, se si misura la complessità in termini di confronti, si ha che l’algoritmo trova il massimo
in una lista di lunghezza n in O(n) passi.
Questo ragionamento ci ha portato ad usare la notazione Big-O introdotta nella sezione
precedente per dare una misura della complessità computazionale temporale dell’algoritmo.
Questa procedura può essere generalizzata allo studio dell’efficienza di qualunque algoritmo
e, negli esempi che seguono, mostreremo come usare tale misura e come sia possibile servirsi
della composizione delle funzioni per valutare la complessità generata da più procedure o
da procedure annidate.
Prendendo ad esempio l’algoritmo di ricerca lineare, all’interno del ciclo while vengono
effettuati due confronti: uno per verificare se si è arrivati alla fine della lista e l’altro per
confrontare x con un termine della lista. Successivamente viene eseguito un confronto fuori
dal ciclo. Considerando il caso peggiore, ovvero quello in cui l’elemento non è contenuto
nella lista, sono eseguiti 2n + 2 confronti e quindi la ricerca lineare richiede almeno O(n)
confronti.
Il tipo di analisi eseguita sull’algoritmo di ricerca lineare è del tipo worst case, ovvero
viene contato il massimo numero di operazioni necessarie per risolvere il nostro problema
dato un input fissato. Ovviamente, quest’analisi mostra quante operazioni sono necessarie
all’algoritmo, nel caso peggiore, per garantire che verrà prodotta una soluzione, ma nella
realtà possono esserne effettuate molte di meno. A titolo di esempio, si può notare che
l’algoritmo di ricerca lineare ha complessità proporzionale a n, ma se l’elemento da ricercare
è tra i primi nella lista, l’algoritmo termina con un numero di passi minore.
Analizziamo ora l’algoritmo di ricerca binaria e, per semplicità, supponiamo che la lista
28
Algoritmi
Complessità
O(1)
O(log n)
O(n)
O(n log n)
O(na )
O(an ), con a > 1
O(n!)
Terminologia
Complessità costante
Complessità logaritmica
Complessità lineare
Complessità n log n
Complessità polinomiale
Complessità esponenziale
Complessità fattoriale
Tabella 2.1: Terminologia comunemente usata per indicare la complessità degli algoritmi
sia composta da n = 2k elementi (e, quindi, k = log n). Notare che con questa ipotesi non
c’è perdita di generalità, perchè potremmo considerare la nostra lista originale come parte
di una lista più grande di 2k+1 elementi, dove 2k ≤ n ≤ 2k+1 . Ad ogni passo dell’algoritmo,
le variabili i e j sono confrontate per vedere se la lista ristretta ha più di un termine e, se
i < j, viene eseguito un confronto per determinare se x è maggiore del termine mediano
della lista in considerazione. Al primo passo, la ricerca è limitata a 2k−1 termini e vengono
effettuati due confronti; ad ogni passo successivo vengono eseguiti due confronti su di una
lista che è grande la metà di quella del passo precedente. Alla fine del ciclo while vengono
eseguiti due ulteriori confronti e quindi complessivamente saranno stati eseguiti al più 2k+2
confronti, ovvero 2 ⌊log n⌋ + 2 confronti. Quindi, l’algoritmo di ricerca binaria richiede al
più O(log n) confronti, e da ciò segue che tale algoritmo, a parità di input, è molto più
efficiente dell’algoritmo di ricerca lineare.
In Tabella 2.1 è riportata la terminologia comunemente usata per indicare la complessità
temporale degli algoritmi. La stima Big-O permette di valutare come il tempo necessario
per risolvere un problema cambi in funzione della dimensione dell’input. Tale stima però
non fornisce indicazioni sul tempo realmente necessario ad un computer per completare
l’elaborazione perchè non possiamo individuare un valore limite senza aver ricavato le
costanti C e k nell’equazione 2.1 ed inoltre perchè è difficile stimare il tempo richiesto per
completare una singola operazione. Comunque, possiamo tentare di fornire una misura
riconducendoci a delle stime sui tempi di operazione sui bit2 . Cosı̀ facendo, possiamo
2
Nel nostro caso si è assunto che il tempo di elaborazione di una operazione base su bit, eseguita su un
computer ad alte prestazioni, sia di 10−9 secondi.
29
Algoritmi
Dimensione
del problema
n
10
102
103
104
105
106
Numero di operazioni su bit eseguite
log n
3 · 10−9 sec
7 · 10−9 sec
1 · 10−8 sec
1.3 · 10−8 sec
1.7 · 10−8 sec
7 · 10−8 sec
n
10
10−7
10−6
10−5
10−4
10−3
−8
sec
sec
sec
sec
sec
sec
n log n
3 · 10−8 sec
7 · 10−7 sec
1 · 10−5 sec
1 · 10−4 sec
2 · 10−2 sec
3 · 10−2 sec
n2
2n
10 sec
10−5 sec
10−3 sec
10−1 sec
10 sec
17 min
10 sec
4 · 1013 anni
∗
∗
∗
∗
−7
−6
n!
3 · 10−3 sec
∗
∗
∗
∗
∗
Tabella 2.2: Tempo di calcolo usato dagli algoritmi
ottenere la Tabella 2.2 che riporta i tempi computazionali necessari a problemi con diverse
dimensioni di input, fornendo inoltre una indicazione sul numero di operazioni su bit. Gli
asterischi indicano tempi maggiori di 10100 anni.
La tabella riporta tempi computazionali che possono risultare impraticabili anche per
istanze piccole. Ci si potrebbe chiedere quale vantaggio si avrebbe con l’aumento delle prestazioni degli elaboratori. Dall’esempio seguente è facile convincersi che questa possibilità
non ha riscontro nella realtà.
Supponiamo di considerare due algoritmi, uno di complessità polinomiale O(n2 ) e l’altro
di complessità esponenziale O(2n ). Consideriamo ora un elaboratore che abbia velocità v1
e che ci permetta di risolvere in una data unità di tempo una istanza di dimensione n1 .
Immaginiamo ora di poter disporre di un elaboratore 100 volte più veloce (v2 = 100 · v1 );
considerando la complessità dell’algoritmo, si può affermare che esiste una proporzionalità
pari a n22 /n21 = v2 /v1 = 100 e quindi, con un rapido conto, si ottiene che è possibile risolvere
nello stesso tempo istanze con n2 = 10 · n1 , cioè 10 volte più grandi.
Applicando lo stesso ragionamento per l’algoritmo di complessità O(2n ), si ottiene,
con un elaboratore 100 volte più veloce, che 2n2 /2n1 = v2 /v1 = 100, ovvero che n2 =
n1 + log 100 ≈ n1 + 7, cioè posso risolvere istanze con solo 7 nodi in più!
Questo esempio ci mostra come il miglioramento delle capacità di elaborazione ha
purtroppo solo un impatto marginale nella efficienza degli algoritmi con complessità
esponenziale.
30
Algoritmi
2.5
Esercizi
Es. 2.5.1 Mostrare che l’algoritmo dell’Esempio 2.2.1 rispetta le Proprietà 2.2.1
Es. 2.5.2 Fornire una stima Big-O della seguente funzione: f (n) = n2 +
n
log n
log n!.
Es. 2.5.3 Fornire una stima Big-O della seguente funzione: f (n) = 13 + 23 + . . . + n3 .
Es. 2.5.4 Fornire una stima Big-O della seguente funzione: f (n) =
√
1+
√
2+...+
√
n.
Es. 2.5.5 Fornire una stima Big-O della seguente funzione: f (n) = log 2 + log 3 + . . . +
log n.
Es. 2.5.6 Dare una stima Big-O per f (n) = n2 (n log(n!) + n log n).
Es. 2.5.7 Dare una stima Big-O per f (n) = (log n)2 + log(n2 ).
Es. 2.5.8 Determinare la complessità computazionale associabile al seguente segmento di
codice (n << m):
while (j < m) do
begin
for i := 1 to m do
if a[i] < j then a[i] = j;
for i := 1 to n do
if a[i] < j then a[i] = j;
j = j + 1;
end
Es. 2.5.9 Determinare la complessità computazionale associabile al seguente segmento di
codice (m << n):
for i := 1 to m do
begin
Algoritmi
if a[i] < j then a[i] = j;
for i := 1 to n do
if a[i] < j then a[i] = j;
j = j + 1;
end
Es. 2.5.10
31
Capitolo 3
Proprietà e strutture dei grafi
3.1
Strutture dati per la rappresentazione di grafi
La performance di un algoritmo su grafo dipende non solo dall’algoritmo stesso, cosı̀ come
è stato descritto nella sezione precedente, ma anche dal modo con il quale il grafo viene
memorizzato e da come vengono gestiti gli aggiornamenti ed i risultati parziali.
Tipicamente, nella rappresentazione di un grafo occorre memorizzare due tipi di informazioni: la topologia dei nodi e degli archi, quindi eventuali grandezze da associare ai nodi
e agli archi stessi(queste informazioni saranno necessarie soprattutto per gli algoritmi che
approfondiremo nelle prossime sezioni).
Prima di introdurre le diverse tipologie di rappresentazione, si vuole estendere e generalizzare il concetto di grafo finora esposto al caso in cui gli archi siano orientati. La
definizione è simile alla Definizione 1.1.1:
Definizione 3.1.1 Un grafo orientato G è una tripla costituita da un insieme di vertici V (G) (detti anche nodi), un insieme di archi orientati E(G) e una relazione, detta
relazione di incidenza, che associa un vertice ad un altro.
Risulta chiaro allora che nel caso di grafi orientati non è più corretto affermare che
e = ab è la stessa cosa di scrivere e = ba, perché la relazione di incidenza è vera solo
in un verso. In ogni caso però, tutte le definizioni di walk, trail, path, ecc che abbiamo
32
33
Proprietà e strutture dei grafi
dato nel Capitolo 1, si estendono immediatamente al caso dei grafi orientati avendo cura
di considerare in modo stringente l’orientamento nelle varie tipologie di cammini (e cicli).
2
2
4
d
a
a
c
1
4
d
g
e
h
b
c
1
g
e
h
b
3
f
5
Figura 3.1: Grafo non orientato
3
f
5
Figura 3.2: Grafo orientato
Le rappresentazioni dei grafi che studieremo sono la matrice di incidenza, la matrice di
adiacenza e la lista di adiacenza.
Matrice di incidenza
La rappresentazione attraverso la matrice di incidenza di un grafo non orientato G di n
nodi ed m archi avviene attraverso una matrice A di dimensione n × m che contiene una
riga per ogni nodo del grafo ed una colonna per ogni arco. La colonna corrispondente
all’arco (i, j) ha soltanto due elementi diversi da zero in corrispondenza dei nodi i e j.
Prendendo il grafo disegnato in Figura 3.1, la sua matrice di incidenza corrispondente è:

1


1


A = 0


0

0

1 0 0 0 0 0 0


0 1 1 0 0 0 0


1 1 0 1 1 0 0


0 0 1 1 0 1 1

0 0 0 0 1 1 1
La matrice di incidenza ha una struttura tipica per cui solo 2m componenti, delle n×m,
sono diverse da zero ed uguali ad uno. È da notare che la somma degli elementi diversi da
zero in una riga eguaglia il grado del nodo corrispondente, ovvero:
34
Proprietà e strutture dei grafi
d(vi ) =
m
X
aij
j=1
dove chiaramente aij è l’elemento della riga i-esima e colonna j-esima.
Nel caso di un grafo orientato la matrice di incidenza si modifica considerando che per
l’h-esimo arco (i, j), nella colonna h corrispondente, l’elemento aih è uguale ad 1 mentre
l’elemento ajh è uguale a −1. Di conseguenza, per il grafo in Figura 3.2 la matrice è:


1
1
0
0
0
0
0
0




−1 0 −1 1
0
0
0
0




A =  0 −1 1
0
1
1
0
0




0
0
0 −1 −1 0 −1 1 


0
0
0
0
0 −1 1 −1
La rappresentazione di un grafo tramite la matrice di incidenza è chiaramente non
efficiente a causa dello spazio necessario per la sua memorizzazione su un elaboratore, soprattutto al crescere di n e della densità. Nonostante ciò, questa rappresentazione è di
grande importanza perchè come vedremo nelle prossime sezioni, oltre a possedere interessanti proprietà teoriche, essa coincide con la matrice dei coefficienti della formulazione dei
problemi di flusso a costo minimo.
Matrice di adiacenza
La rappresentazione attraverso la matrice di adiacenza di un grafo non orientato G di n
nodi ed m archi avviene attraverso una matrice A di dimensione n × n dove ogni riga
ed ogni colonna corrispondono ai nodi e gli elementi della matrice aij sono o 0, se non
esiste l’arco (i, j) corrispondente, oppure sono uguali alla somma del numero di archi che
uniscono i nodi (i, j). Quindi, se riprendiamo l’esempio in Figura 3.1, la sua matrice di
adiacenza è:
35
Proprietà e strutture dei grafi

0


1


A = 1


0

0
1 1 0
0 1 1
1 0 1
1 1 0
0 1 2

0


0


1


2

0
La matrice di adiacenza per i grafi non orientati è simmetrica ed ha n2 elementi. Conseguentemente, questa rappresentazione è efficiente solo se il grafo è significativamente denso,
mentre per grafi sparsi si ha un notevole spreco di memoria.
Analizzando la matrice di adiacenza, si può contare il numero di archi incidenti sul
nodo i semplicemente sommando gli elementi della riga i-esima (quindi d(v5 ) = 3, pari alla
somma del contributo dell’arco adiacente al nodo 3 e dei due archi adiacenti al nodo 4).
Analogamente, il numero di archi incidenti su un nodo j si ottiene sommando gli elementi
della colonna j-esima. Da questo ne consegue che la somma di tutti gli elementi diversi da
zero della matrice di adiacenza è uguale a 2m.
Nel caso di grafi orientati, invece, la matrice di adiacenza perdela sua caratteristica di
simmetria e, per il grafo in Figura 3.2, diventa:

0


0


A = 0


0

0
1 1 0
0 0 1
1 0 1
0 0 0
0 0 1

0


0


1


1

0
Nella matrice di adiacenza nel caso di grafi orientati è possibile contare il numero di
archi uscenti da un nodo i (tale insieme è chiamato anche stella uscente) sommando gli
elementi della riga corrispondente, mentre il numero di archi entranti in un nodo i (tale
insieme è chiamato anche stella entrante) si ottiene sommando gli elementi della colonna
corrispondente. Da questo ne consegue che gli elementi diversi da zero sono uguali a m.
Proprietà e strutture dei grafi
36
Lista di adiacenza
La lista di adiacenza dei nodi A(i) di un grafo G con n nodi, è un vettore di dimensione n nel
quale ogni cella i punta una lista che contiene l’insieme dei nodi j tali per cui (i, j) ∈ E(G).
In Figura 3.3 è rappresentata la lista di adiacenza del grafo in Figura 3.2.
La lista di adiacenza di un grafo è molto utile quando, agli archi ed ai nodi, assoceremo dei numeri che ne rappresenteranno il costo, la lunghezza, ecc, come illustreremo
nei prossimi Capitoli. Infatti, mediante questa rappresentazione vedremo che sarà sufficiente aumentare, per ogni cella puntata dal nodo, i campi della lista corrispondente per
memorizzare tutte le informazioni necessarie.
Figura 3.3: Lista di adiacenza per il grafo di Figura 3.2.
3.2
Isomorfismo tra grafi
Osservando la matrice di adiacenza per i grafi non orientati, si può notare come si introduca
implicitamente un ordinamento dei vertici per riga. In realtà, quello che si vuole è ottenere
delle proprietà che non siano funzione di tale ordinamento. Intuitivamente, si può dire che
si vorrebbero trovare due diverse rappresentazioni matriciali del grafo G tali per cui siano
preservate le relazioni di adiacenza.
Definizione 3.2.1 Due grafi G = (V, E) e G′ = (V ′ , E ′ ) si dicono isomorfi, e si scrive
G∼
= G′ , se esiste una corrispondenza biunivoca tra i due insiemi di vertici, cioè se esiste
una biiezione Φ : V → V ′ tale che xy ∈ E ⇔ φ(x)φ(y) ∈ E ′ .
37
Proprietà e strutture dei grafi
Per esempio, se prendiamo le matrici di adiacenza dei due grafi G e G′ , rappresentati
in Figura 3.4, possiamo notare che è facile individuare una funzione biiettiva che mette in
corrispondenza i vertici dei due grafi e che le due matrici si trasformano l’una nell’altra
mediante permutazioni delle righe e delle colonne. Al lettore è lasciato per esercizio di
trascrivere le due matrici e di trovare la funzione Φ che permette di passare da una matrice
all’altra.
a
1
2
3
4
G′
G
d
b
c
Figura 3.4: Esempio di due grafi isomorfi
Dati due grafi generici G e H ci possiamo chiedere ora come verificare che siano tra
di loro isomorfi. Alcune condizioni necessarie possono essere facilmente individuate; per
esempio i due grafi devono avere stessi ordine e dimensione, oppure l’immagine di un vertice
i ∈ G deve avere in H lo stesso grado dell’immagine i ∈ H in G.
In generale però lo studio esaustivo delle permutazioni delle matrici di adiacenza di due
grafi di ordine n non costituisce un algoritmo molto efficiente, perchè questo avrebbe una
complessità computazionale di O(n!). Dato il grande interesse che il problema dell’isomorfismo tra grafi riveste nelle applicazioni pratiche, sono stati proposti molti algoritmi per
tentare di risolverlo, ma esso rimane ancora aperto, nel senso che non è ancora stato proposto un algoritmo efficiente ne si è riuscito a dimostrare che tale algoritmo possa esistere
oppure no [6].
Esempio 3.2.1 Siano assegnati i due grafi in Figura 3.5. Dimostrare che sono tra loro
isomorfi.
Sia definita la funzione Φ : V (G) → V (H) tale che φ(w) = a, φ(x) = c, φ(y) = b
e φ(z) = d. Per mostrare che Φ è un isomorfismo occorre analizzare se tali funzioni
38
Proprietà e strutture dei grafi
w
z
G
c
d
b
a
H
x
y
Figura 3.5: Due grafi isomorfi
preservino le adiacenze, e questo può essere testato riscrivendo la matrice A(G) in modo
che permutando righe e colonne si ottenga la matrice A(H).
Nel nostro caso si può notare come, permutando tra di loro la seconda e la terza colonna
di A(G) e poi la terza e la quarta (e la stessa cosa si fa per le righe), si ottiene la matrice
del grafo H, dimostrando quindi l’isomorfismo tra i due grafi. Per esteso:



0 1 0 0
0






1 0 1 0
0



A(G) = 
→


0 1 0 1
0



0 0 1 0
1


0 0 1
0




0
0 1 1
→


0
1 0 0


1 0 0
1

0 0 1


0 1 1
 = A(H)

1 0 0

1 0 0
Classi di isomorfismo
Il concetto di isomorfismo visto come “G e H sono isomorfi” ha un valore limitato se non
lo estendiamo all’applicazione su insiemi di grafi che siano tra loro isomorfi.
Ricordando che una relazione su un insieme I è una collezione di coppie ordinate
di suoi elementi e una relazione di equivalenza è una relazione riflessiva (a ∼
= a),
simmetrica (a ∼
=b⇔b∼
= a) e transitiva (a ∼
= b, b ∼
= c, ⇒ a ∼
= c), possiamo affermare che:
Proposizione 3.2.1 La relazione di isomorfismo è una relazione di equivalenza.
Dimostrazione: Lasciata per esercizio al lettore.
39
Proprietà e strutture dei grafi
Una relazione di equivalenza partiziona un insieme in classi di equivalenza e quindi
due elementi appartengono alla stessa classe se e solo se ricadono nella stessa classe di
equivalenza.
Definizione 3.2.2 Una classe di isomorfismo di grafi è una classe di equivalenza di grafi
sotto una relazione di isomorfismo.
Per esempio, i grafi completi Kn costituiscono delle classi di isomorfismo di grafi perchè,
prese due a due, rispettano le proprietà delle relazioni di equivalenza. Un’altra importante
classe di isomorfismo è costituita dai grafi bipartiti completi , ovvero quei grafi semplici
bipartiti tali che due vertici sono adiacenti se e soltanto se appartengono a partizioni
diverse. Se gli insiemi partizioni hanno dimensione p e q, il grafo bipartito completo è
indicato come Kp,q .
K2,3
K3,3
Figura 3.6: I grafi bipartiti completi K2,3 e K3,3 .
3.3
Connessione, cicli e grafi bipartiti
Nelle prossime definizioni ci dirigeremo verso le classi di equivalenza delle relazioni di
connessione limitandoci al caso dei grafi non orientati. Riprendendo i concetti definiti alla
fine della Sezione 1.2.4, diremo che un sottografo connesso di G è massimale se è connesso
e non è contenuto in nessun altro sottografo connesso di G.
Definizione 3.3.1 Dato un grafo G, le sue componenti connesse sono i suoi sottografi
connessi massimali.
In Figura 3.7 è rappresentato un grafo con quattro componenti connesse, {a, b},
{c, d, e, f }, {g} e {h, i, j, }. Come si può notare, la componente connessa {g} è costituita
da un solo nodo e viene chiamata anche componente triviale.
40
Proprietà e strutture dei grafi
a
c
b
e
d
f
g
i
j
h
Figura 3.7: Un grafo con quattro componenti connesse.
Definizione 3.3.2 Un cut-vertex è un vertice la cui rimozione aumenta il numero di
componenti connesse.
Un bridge è un arco la cui rimozione aumenta il numero di
componenti connesse.
Quindi riprendendo la Figura 3.7, un cut-vertex è per esempio il nodo i, mentre l’arco
ij è un bridge (infatti, in entrambi i casi passo da 4 a 5 componenti connesse).
Proposizione 3.3.1 Ogni grafo G con n vertici ed m archi ha almeno n − m componenti
connesse.
Dimostrazione: Supponiamo sia assegnato un grafo con n nodi e nessun arco. In questo
caso ho esattamente n componenti connesse. Per la Definizione 3.3.2, aggiungendo un arco
diminuisco di uno il numero di componenti connesse, quindi dopo aver aggiunto m archi il
numero di componenti è diminuito a n − m.
Esempio 3.3.1 Sia G un grafo con n vertici tale che ogni suo vertice abbia grado almeno
(n − 1)/2. Mostrare che G è connesso.
Per dimostrare la connessione, ipotizziamo per assurdo che ci siano due nodi x ed y di G
appartenenti a due diverse componente connesse ed aventi grado ciascuno almeno (n−1)/2.
Sia la componente di x quella in Figura 3.8. Allora il numero di nodi di tale componente
connessa deve essere 1 + (n − 1)/2 = (n + 1)/2. Tale numero di nodi si ottiene ovviamente
anche nell’altra componente connessa.
Ora, per ottenere il numero di nodi del grafo di partenza basta sommare il numero di
nodi delle due componenti connesse, ma cosı̀ facendo ci accorgiamo che otteniamo (n +
41
Proprietà e strutture dei grafi
1)/2+(n+1)/2 = n+1, che eccede il numero di nodi di partenza di G, mostrando l’assurdo.
x
n−1
2
Figura 3.8: Componente connessa di x
Definizione 3.3.3 Se a e b sono due vertici appartenti alla stessa componente connessa,
allora esiste un path di lunghezza minima d(a, b) chiamato distanza.
Lemma 3.3.2 Ogni walk chiuso W di lunghezza dispari contiene un ciclo dispari.
Dimostrazione: La dimostrazione è per induzione. Per l = 1 il lemma è ovvio; per l > 1
si ponga l’ipotesi induttiva affermando che il lemma è vero per walk chiusi più corti di W .
In questo caso si possono presentare due possibilità: se non ho walk chiusi ripetuti
allora ho già cicli dispari ed il lemma è dimostrato; altrimenti, se ho nodi ripetuti (per
esempio il nodo i, come in Figura 3.9) allora posso spezzare W in due walk chiusi, uno di
lunghezza pari e l’altro di lunghezza dispari. Ma per il walk chiuso di lunghezza dispari
vale l’ipotesi induttiva e quindi deve contenere un ciclo di lunghezza dispari.
Pari
Dispari
i
Figura 3.9: Un circuito con un nodo ripetuto.
Le definizioni ed i teoremi che seguono caratterizzano ed esaminano la connessione nei
grafi.
Proprietà e strutture dei grafi
42
Esempio 3.3.2 Sia G un grafo con n ≥ 2 vertici, n − 1 archi e senza vertici isolati.
Mostrare che G contiene almeno due vertici di grado 1.
Per il Lemma 1.1.1,
con d(vi ) = 1.
P
v
d(vi ) = 2m = 2n − 2, quindi ci devono essere almeno due vertici
Definizione 3.3.4 Un grafo G si dice aciclico se non contiene cicli.
Lemma 3.3.3 Ogni grafo connesso G di ordine n contiene almeno n − 1 archi.
Dimostrazione: Dimostriamo il lemma per induzione. Per n = 1 il lemma è ovvio.
Per n ≥ 2, considero un vertice arbitrario v ∈ G e considero il grafo H = G \ v, non
necessariamente connesso, con Fi componenti connesse aventi ciascuna ni vertici, ovvero
n1 + n2 + . . . + nk = ni − 1. Per ipotesi induttiva, affermo che il sottografo di H indotto
su Fi ha almeno n − 1 archi.
Ora, osservo che il vertice v deve essere connesso ad ognuna delle k componenti Fi con
almeno un arco; quindi G contiene almeno (n1 − 1) + (n2 − 1) + . . . + (nk − 1) + k =
n − 1 − k + k = n − 1 archi.
Lemma 3.3.4 Ogni grafo aciclico ha, al più, n − 1 archi.
Dimostrazione: Dimostriamo il lemma per induzione. Per n = 1 il lemma è ovvio.
Per n ≥ 2, prendo un arco xy ∈ G. Quindi il grafo H = G \ xy ha esattamente una
componente connessa in più di G (questo perché, per ipotesi, il grafo è aciclico e quindi tra
x e y non può esserci un altro path), ovvero, H può essere decomposto in grafi connessi
aciclici H1 , H2 , . . . , Hk . Per ipotesi induttiva, affermo che ogni grafo Hi contiene al più
ni − 1 archi, dove ni è il numero di nodi di ogni componente connessa, i = 1, . . . , k.
Allora G ha al più (n1 −1)+(n2 −1)+. . .+(nk −1)+1 = (n1 +n2 +. . .+nk )−(k−1) ≤ n−1
archi.
Teorema 3.3.5 Sia G un grafo di ordine n. Allora due qualunque delle seguenti affermazioni implicano la terza:
Proprietà e strutture dei grafi
43
a) G è connesso;
b) G è aciclico;
c) G ha n − 1 archi.
Dimostrazione:
a, b ⇒ c Se G è connesso ed aciclico, allora per il Lemma 3.3.3 e per il Lemma 3.3.4 il grafo
ha esattamente n − 1 archi;
a, c ⇒ b Supponiamo G connesso e con n−1 archi e che, per assurdo, abbia un ciclo C. Allora
H = G \ xy, con xy ∈ C, è un grafo connesso di n vertici e n − 2 archi e questo
contraddice il Lemma 3.3.3;
b, c ⇒ a Supponiamo che G sia aciclico ed abbia n−1 archi. Per il Lemma 3.3.4 non ho vertici
isolati (altrimenti avrei n − 1 nodi e n − 1 archi), quindi per il Lemma 1.1.1 il grafo
G ha almeno un vertice di grado uno ed abbiamo che G \ v è un grafo aciclico con
n − 1 vertici e n − 2 archi. Per induzione, G \ v è connesso e quindi anche G.
Grafi bipartiti
L’enunciato del Lemma 3.3.2 ci tornerà utile nella dimostrazione del prossimo teorema che
ci permetterà di riconoscere la bipartizione di un grafo. Ricordiamo dalla Sezione 1.2.2
che un grafo G si dice bipartito se l’insieme dei suoi nodi V (G) è l’unione di due insiemi
indipendenti disgiunti V1 e V2 chiamati insiemi partizione ed ogni suo arco va da V1 a V2 .
Teorema 3.3.6 Un grafo G è bipartito se e soltanto se non contiene cicli dispari.
Dimostrazione: Necessità. Sia G un grafo bipartito. Ogni walk alterna i propri archi tra
le due partizioni di nodi per cui ogni circuito deve avere un numero di archi pari, quindi
G non ha cicli dispari.
44
Proprietà e strutture dei grafi
Sufficienza. Sia G un grafo senza cicli dispari. Poiché un grafo è bipartito solo e soltanto
se lo sono le sue componenti, possiamo assumere allora il grafo G connesso. Si prenda un
vertice x ∈ V e poniamo V1 = {y : d(x, y) sia pari} e V2 = V \ V1 . Si può notare allora che
non ci sono archi che uniscono due nodi in V1 , altrimenti G dovrebbe contenere un ciclo
dispari. Quindi G è bipartito.
3.4
Alberi
I grafi che soddisfano le condizioni del Teorema 3.3.5 costituiscono una classe di grafi molto
particolari chiamati alberi .
Definizione 3.4.1 Un albero è un grafo connesso che non contiene cicli. Ogni vertice
di grado uno di un albero è chiamata foglia. Una foresta è un grafo le cui componenti
connesse sono alberi.
Proposizione 3.4.1 Per un albero sono vere le seguenti affermazioni:
1. Un albero con n nodi contiene n−1 archi; una foresta di ordine n e con k componenti
contiene n − k archi.
2. Un albero ha almeno due foglie.
3. Ogni coppia di nodi di un albero è connessa da un solo path.
Figura 3.10: Una foresta
Figura 3.11: Un albero
Le definizioni e le preposizioni date possono essere riassunte nel seguente teorema:
45
Proprietà e strutture dei grafi
Teorema 3.4.2 Dato un grafo G, le seguenti affermazioni sono equivalenti:
1. G è un albero.
2. G è connesso e se xy ∈ G, allora G − xy è sconnesso, ovvero ogni arco è un bridge.
3. G è aciclico e se x e y sono due vertici non adiacenti di G, allora G + xy contiene
un ciclo.
Il seguente corollario ci fornisce una proprietà che useremo nel seguito:
Corollario 3.4.3 Ogni grafo connesso G contiene un albero ricoprente, ovvero un albero
che contiene tutti i nodi del grafo (denominato anche spanning tree).
Dimostrazione: È sufficiente costruire un sottografo di G connesso e aciclico di n − 1
archi e che ricopre ogni nodo.
Riprenderemo lo studio di questa classe di grafi nella Sezione 5.3 dove verranno proposti
degli algoritmi per la ricerca di alberi ricoprenti.
3.5
Cammini Euleriani
In questo paragrafo risolveremo il Problema dei Ponti di Könisberg introdotto nel Capitolo 1 nel caso di grafi arbitrari. Quanto diremo sui cammini euleriani si estende anche ai
grafi non semplici, ovvero ai grafi che possiedono archi paralleli o cappi e che in letteratura
vengono chiamati anche multigrafi.
Definizione 3.5.1 Un trail euleriano (tour di Eulero) è un trail (trail chiuso) che
contiene ogni arco di G esattamente una volta.
Definizione 3.5.2 Un grafo è chiamato Euleriano se contiene un tour di Eulero.
Enunciamo ora il Teorema di Eulero (1736) che caratterizza i grafi Euleriani.
Teorema 3.5.1 Sia G un multigrafo connesso.
equivalenti:
Allora le seguenti affermazioni sono
Proprietà e strutture dei grafi
46
1. Il grafo G è euleriano.
2. Ogni vertice di G ha grado pari.
3. Gli archi di G possono essere partizionati in cicli.
Dimostrazione:
(1) ⇒ (2) Se G è euleriano, allora ogni vertice aggiunge un contributo di 2 al ciclo. Dato che
ogni arco compare esattamente una volta, ogni vertice v deve avere grado pari
(2) ⇒ (3) Supponiamo che G abbia n vertici. Dato che il grafo è connesso, ha almeno n−1 archi
per il Lemma 3.3.3, mentre per l’Esempio 3.3.2 deve avere almeno n archi; quindi per
il Lemma 3.3.4 c’è un ciclo C in G. Rimuoviamo ora C, ottenendo un nuovo grafo H
nel quale ogni vertice ha ancora grado pari. Considerando separatamente le componenti connesse di H, possiamo ripetere il ragionamento per ognuna delle componenti
e quindi, proseguendo, ci arrestiamo quando abbiamo terminato la decomposizione.
Quindi gli archi di G possono essere partizionati in cicli.
(3) ⇒ (1) Sia C uno dei cicli della partizione di E(G) in cicli. Se C è già euleriano, abbiamo
la tesi. Altrimenti esiste un altro ciclo C ′ che ha un vertice v in comune con C.
Possiamo allora, senza perdita di generalità, considerare v come vertice di partenza
e di fine di entrambi i cicli C e C ′ in modo che CC ′ sia ancora un tour. Continuando
con la stessa procedura otteniamo un tour d’Eulero
Esempio 3.5.1 Supponiamo sia assegnata la porzione di scacchiera cosı̀ come disegnato
nell’immagine a sinistra della Figura 3.12. Considerando che nelle quattro caselle bianche
agli angoli ci sono i quattro cavalli, sopra i due bianchi (chiamati CB) e sotto i due neri
(chiamati CN), è possibile, usando le mosse ammesse per i cavalli negli scacchi, invertire
le posizioni in modo che dopo un certo numero di mosse i due cavalli bianchi siano nelle
caselle inferiori e quelli neri nelle caselle superiori?
47
Proprietà e strutture dei grafi
Per risolvere questo problema basta modellare la porzione di scacchiera con un grafo dove
i nodi sono le caselle ed un arco esiste tra due nodi se esiste un trasferimento lecito tra le
due caselle.
CB
CB
CN
CN
Figura 3.12: Come spostare i quattro cavalli.
Allora, dato il grafo (rappresentato a destra in Figura 3.12), è sufficiente individuare un
cammino lungo il grafo in modo che i quattro cavalli, spostandosi, arrivino nella posizione
finale richiesta. È facile convincersi che tale percorso esiste se c’è un ciclo euleriano sul
grafo, cosa immediatamente verificabile osservando che i nodi hanno tutti grado pari.
3.6
Cammini Hamiltoniani
3.7
Esercizi
Es. 3.7.1 Date la seguente matrice di incidenza, disegnare i grafi corrispondenti:
Es. 3.7.2 Date la seguente matrice di adiacenza, disegnare i grafi corrispondenti:

0


0


0


0
A1 = 

0


0


0

0
1 1
1 0 0
0 0
0 1 0
1 0
0 0 0
0 0
0 0 0
0 0
0 0 0
1 1
0 1 0
0 0
0 0 0
0 0
0 0 1


0




0
0 0




0
0 0




0
1 0
 A2 = 


0
0 1




0
1 0




0
0 1


0 0
0
0 0
1
1 1
0 0
0
0 0
1 0
0
0 0
0 0
0
1 0
0 0
0
0 0
0 0
0
1 1
1 0
0
0 1
0 0
0
0 0
0 1
Es. 3.7.3 Mostrare che P4 è isomorfo al suo complemento.
0 0



0 0


0 0


0 0


0 1


1 0


0 1

0 0
Proprietà e strutture dei grafi
48
Es. 3.7.4 Disegnare tutte le classi di isomorfismo costituite da grafi con n = 5 e m = 5,
anche non connessi.
Es. 3.7.5 Si disegnino tutte le classi di isomorfismo formate da spanning tree di K3,3 .
Quante sono in tutto?
Es. 3.7.6 Si disegnino tutti le possibili classi di grafi di 4 nodi non isomorfi.
Es. 3.7.7 Si disegnino tutti le possibili classi di grafi di 6 nodi non isomorfi.
Es. 3.7.8 Quanti sono gli alberi che posso ottenere, a meno di isomorfismi, legando con
due archi i due vertici n − 1 e n al path 1, 2, . . . , n − 2 ?
Es. 3.7.9 A partire da K4 , togliendo quattro archi, quanti grafi connessi posso ottenere?
E quanti sconnessi?
Es. 3.7.10 Dire quanti sono gli alberi con 4 vertici.
Es. 3.7.11 Sia assegnato un albero G con n vertici aventi grado pari a: d(v1 ) = d(v2 ) =
d(v3 ) = 1, d(vn ) = 3 e d(v4) = d(v5) = . . . = d(vn−1 ) = 2 e con l’arco (2, n) ∈ E(G).
Quanti alberi diversi si possono ottenere legando gli archi in modo da soddisfare i dati
imposti?
Es. 3.7.12 Sia assegnato il grafo in figura, composto da un ciclo di n1 nodi, un ciclo di
n2 nodi e un terzo ciclo da n3 nodi, tutti e tre uniti in un solo vertice. In quanti modi
posso trovare un albero togliendo contemporaneamente tre archi, uno per ogni ciclo? (N.B.
I nodi dei cicli nel disegno sono solo indicativi, si deve considerare il caso generale).
Proprietà e strutture dei grafi
49
Capitolo 4
Grafi Planari
Per facilitare la trattazione dei grafi planari iniziamo dal seguente problema: tre acerrimi
nemici hanno tre case vicine ed hanno contemporaneamente la necessità di doversi allacciare
le loro abitazioni alle forniture di energia elettrica, gas ed acqua. Per evitare qualsiasi
discussione esigono che nessuna delle connessioni di ognuno di loro si intersechi in qualunque
punto con quelle degli altri due. La situazione è rappresentata in Figura 4.1.
Il problema posto può essere modellato mediante un grafo bipartito completo K3,3 (vedi
Figura 4.2) e quindi la domanda che ci vogliamo porre è la seguente: è possibile disegnare
K3,3 senza intersezioni tra gli archi? In questa sezione ci occuperemo della trattazione dei
grafi che possono essere disegnati su di un piano senza intersezioni tra archi e dimostreremo
che il problema dei tre nemici non ha soluzione.
elettricità
gas
acqua
Figura 4.1: Il problema dei tre nemici.
50
51
Grafi Planari
elettricità
gas
acqua
Figura 4.2: Connessioni per il problema dei tre nemici.
4.1
Grafi sul piano
I concetti che verranno esposti si basano sulla semplice osservazione che ogni curva chiusa
disposta su di un piano divide il piano stesso in due regioni distinte, ovvero la regione
interna alla curva e la regione esterna alla curva. Questa osservazione elementare ha senso
se ci limitiamo ad una trattazione semplice della Teoria dei Grafi, mentre, se volessimo
fornire maggiori dettagli, dovremmo introdurre concetti di topologia che esulano dagli
scopi di queste note. Il lettore che volesse approfondire questi argomenti può trovare una
trattazione più approfondita in [5] e [13].
Proposizione 4.1.1 K5 e K3,3 non possono essere disegnate sul piano senza intersezioni
tra archi.
Dimostrazione: Consideriamo un disegno di K5 sul piano come in Figura 4.3 e consideriamo un ciclo ricoprente C. Se non ci sono intersezioni, allora C è disegnato come una
curva chiusa e le corde di C possono essere disegnate fuori e dentro tale curva. Due corde
sono in conflitto se i loro estremi in C sono alternati e se ciò accade è possibile disegnarne
una internamente a C ed una esternamente a C. Se il ciclo è di lunghezza cinque, due corde possono essere tracciate internamente e due esternamente, ma dato che ci sono cinque
corde se ne deduce che è impossibile completare il disegno.
52
Grafi Planari
K5
K3,3
Figura 4.3: K5 e K3,3 .
Analogamente, nel disegno di K3,3 della Figura 4.3 ci sono tre corde in mutuo conflitto
nel ciclo esterno di lunghezza sei e quindi la corda rimanente rende impossibile il completamento del disegno.
Definizione 4.1.1 Il disegno di un grafo G(V, E) è una funzione f definita su V ∪ E tale
che f : v ∈ V → f (v), e ∈ E → f (e). Un punto f (e) ∩ f (e′ ) è chiamato intersezione.
Quindi, per disegno di un grafo intendiamo una funzione che assegna ad ogni vertice
v un punto f (v) del piano ed assegna ad ogni arco e di vertici u e v una curva f (e). Si
può osservare che possiamo utilizzare G come nomenclatura sia per un grafo che per il suo
disegno; questo perché le relazioni di adiacenza sono chiaramente rispettate e, quindi, un
disegno del grafo G può essere visto come un membro della classe di isomorfismo contente
G.
Dato un disegno di un grafo G, possiamo pensare di spostare gli archi sul piano in modo
da assicurarci che non esistano tre archi che abbiano un punto interno in comune, che un
arco contiene solo i vertici che costituiscono i suoi estremi e che non ci sono archi tra di
loro tangenti. Nel seguito considereremo solo disegni con tali proprietà.
Definizione 4.1.2 Un grafo G si definisce planare se ha un disegno senza intersezioni.
Tale disegno è un embedding planare di G. Un grafo piano è un particolare embedding
planare di un grafo planare.
53
Grafi Planari
Il termine embedding1 planare ribadisce il fatto che il disegno di un grafo G sul piano
è composto da punti del piano stesso, definiti dalla funzione f . Di conseguenza, allo stesso
grafo G possono corrispondere differenti funzioni che definiscono embedding diversi.
Un embedding planare divide il piano in parti che saranno ora oggetto del nostro studio.
Definizione 4.1.3 Un insieme aperto nel piano è un insieme A ∈ R2 tale che ∀p ∈ A,
∃r : ||p − r|| < ǫ, ǫ > 0, ⇒ r ∈ A.
Definizione 4.1.4 Una regione è un insieme aperto A che contiene una poligonale di
estremi u e v, ∀u, v ∈ A.
Definizione 4.1.5 Le facce Fi di un grafo planare sono le regioni massimali del piano
che contengono punti del piano che non sono interessati dall’embedding planare.
Sulla base di questa definizione, un grafo planare partiziona il piano in un numero di
regioni mutuamente disgiunte che sono quelle racchiuse all’interno delle poligonali dell’embedding, più la faccia esterna, ovvero la faccia illimitata costituita dalla restante parte di
piano.
F2
F3
F4
F1
Figura 4.4: La facce di un grafo planare.
Per esempio, considerando l’embedding planare di Figura 4.4, questo divide il piano in
quattro regioni costituite dall’insiemi aperti con frontiera gli archi di G e che definiscono
le facce triangolari F1 e F2 , la faccia quadrata F3 ed infine la faccia esterna F4 .
4.2
Grafi duali
Da ogni grafo planare G è possibile costruire un grafo planare duale G∗ ad esso legato.
1
In inglese il termine embedding significa incastrato, incastonato.
54
Grafi Planari
Definizione 4.2.1 Il grafo duale G∗ (V ∗ , E ∗ ) di un grafo piano G(V, E) è un grafo piano
che ha i vertici corrispondenti con le facce di G, mentre i suoi archi corrispondono a quelli
di G nel seguente modo: se e = (u, v) è un arco di G con facce X da un lato e Y dall’altro,
allora i vertici dell’arco duale e∗ ∈ E ∗ sono x ed y che rappresentano le facce X ed Y di
G. Il grado del vertice x ∈ V ∗ è uguale al numero di archi frontiera della faccia X di G.
x
e∗
v
e
u
y
Figura 4.5: Costruzione del duale.
La Figura 4.5 schematizza questo processo, mostrando la costruzione degli archi nel
grafo duale. Utilizzando questo metodo, si può costruire per esempio il duale di K4 e si
può notare come le sue quattro facce portino ad ottenere un duale che è ancora K4 .
4.2.1
Proprietà dei grafi planari e Formula di Eulero
Nel processo di costruzione dei grafi duali può verificarsi la comparsa di loop ed archi
multipli. Per esempio, la costruzione del duale (rappresentato con i vertici bianchi e archi
tratteggiati) per l’embedding in Figura 4.6 porta ad avere due vertici, uno per la faccia
interna triangolare ed uno per la faccia esterna, ed archi corrispondenti alle frontiere tra
le diverse facce.
Figura 4.6: Un esempio di grafo G e del suo duale G∗ .
Grafi Planari
55
È da notare che i loop si formano in corrispondenza di un bridge dato che le facce sui
due lati sono le stesse, mentre archi multipli si hanno in corrispondenza di facce che hanno
più di un arco frontiera in comune. Basandoci su ragionamenti simili possiamo provare che
il duale del duale (G∗ )∗ è isomorfo a G se e soltanto se G é connesso.
Figura 4.7: Esempio di due embedding planari con duali non isomorfi.
Definizione 4.2.2 La lunghezza l(Fi ) di una faccia Fi in un grafo G planare é pari alla
lunghezza del walk chiuso che é frontiera della faccia.
Per esempio nel grafo in alto nella Figura 4.7, le lunghezze delle facce sono rispettivamente 3, 6, 7, mentre nel grafo in basso le lunghezze sono 3, 4, 9. In entrambe i casi si
può notare che la somma fa 16, cioè il doppio del numero degli archi. Questo suggerisce la
seguente proposizione:
Proposizione 4.2.1 Se l(Fi ) denota la lunghezza della faccia Fi del grafo piano G, allora
P
2m(G) = l(Fi ).
Dimostrazione: La lunghezza delle facce corrisponde al grado dei vertici duali relativi e,
P
dato che |m(G)| = |m(G∗ )|, la formula 2m(G) = l(Fi ) altro non è che una diversa forma
del Lemma 1.1.1 (lemma handshaking).
Grafi Planari
56
Questa proposizione evidenzia che le relazioni relative a grafi piani connessi diventano
relazioni per il loro duale quando scambiamo il ruolo dei vertici e delle facce. Infatti,
archi incidenti su vertici diventano archi frontiera di una faccia e viceversa; analoga cosa si
verifica per il ruolo della lunghezza delle facce e grado dei vertici. Un’ulteriore relazione si
può presentare nella colorazione dei vertici di G∗ in termini di G. Di fatto, gli archi di G∗
rappresentano frontiere condivise tra facce di G, quindi il numero cromatico di G∗ eguaglia
il numero di colori necessari per colorare le facce di G. Dato però che il duale del duale
di un grafo piano connesso è il grafo originale, allora 4 colori sono sufficienti per colorare
correttamente le regioni di ogni grafo planare se e soltanto se ogni grafo planare ha numero
cromatico al più 4.
Continuando nella nostra caratterizzazione dei grafi planari, dimostriamo il seguente
teorema.
Teorema 4.2.2 Le seguenti affermazioni sono equivalenti per un grafo piano G:
1. G é bipartito;
2. ogni faccia di G ha lunghezza pari;
3. il duale G∗ è euleriano.
Dimostrazione:
(1) ⇒ (2) Se il grafo é bipartito, allora per il Teorema 3.3.6 non ho cicli dispari e quindi il
contributo alla misura della lunghezza di una faccia è sempre pari.
(2) ⇒)(1) Consideriamo il ciclo C nel grafo G di Figura 4.8. Dato che non ho intersezioni tra
archi, allora C é una curva chiusa che racchiude la regione F ed inoltre tutte le regioni
di G o sono completamente all’interno della regione F o completamente all’esterno;
se sommo la lunghezza delle regioni contenute in F ottengo un numero pari, per
ipotesi. Questa somma contiene sia archi di C, che contano una sola volta, sia ogni
arco contenuto in F , contati due volte (perché insistono su due facce). Sottraendo a
tale somma i contributi degli archi contenuti in F deve rimanere ancora una quantità
pari; quindi C ha lunghezza pari e da ciò consegue la bipartibilità di G.
57
Grafi Planari
C
Figura 4.8: Il ciclo C evidenziato per la dimostrazione del teorema.
(2) ⇔ (3) Il grafo duale G∗ è connesso ed il grado dei sui vertici corrisponde alla lunghezza
delle facce che è pari. La tesi segue subito dal Teorema 3.5.1.
Dimostriamo ora la Formula di Eulero che rappresenta uno dei più importanti risultati
per i grafi planari e che mette in relazione vertici, archi e facce.
Teorema 4.2.3 (Eulero, 1758) Se un grafo piano G connesso ha esattamente n vertici,
m archi e f facce, allora
n−m+f =2
(4.1)
Dimostrazione: Dimostriamo il teorema per induzione. Per n = 1, se m = 0 allora la
formula vale (1 − 0 + 1 = 2), mentre qualunque inserimento di un arco aggiunge un loop
che porta ad avere un arco in più ed una faccia in più; dato che questi ultimi nella formula
si elidono, questa vale qualunque sia il numero di archi.
e
→
Figura 4.9: Contrazione dell’arco e.
Per ipotesi induttiva, affermiamo che il teorema sia valido per m − 1, ∀G connesso.
Allora, se il grafo è connesso, posso trovare un arco e che non è un loop. Se contraggo tale
arco come in Figura 4.9 ottengo un nuovo grafo G′ con n′ = n − 1 vertici, m′ = m − 1 archi
58
Grafi Planari
e f ′ = f facce. Per l’ipotesi induttiva posso scrivere che:
n − m + f = (n − 1) − (m − 1) + f ′ = n′ − m′ + f ′ = 2
Il teorema di Eulero ha notevoli implicazioni nello studio dei grafi planari e ci porta a
dire, per esempio, che tutti gli embedding planari di un grafo connesso G hanno lo stesso
numero di facce o che, sebbene il duale dipenda dal particolare embedding scelto, il numero
dei vertici del duale è invece indipendente.
La formula di Eulero può essere generalizzata al caso di grafi k-connessi attraverso la
formula n − m + f = k + 1.
4.2.2
Caratterizzazione dei grafi planari
Un problema molto importante è, dato un grafo G qualunque, capire se questo possiede un
embedding in un piano, ovvero se è planare. Data l’importanza pratica dei grafi planari,
che compaiono in molte modellizzazioni nella produzione industriale e nella progettazione
dei circuiti VLSI, occorre trovare un modo efficace per certificare la planarità.
Un importante risultato che fornisce un test di planarità è il Teorema di Kuratowski,
che in forma semplificata ha il seguente enunciato.
Teorema 4.2.4 (Kuratowski, 1930) Un grafo è planare se e solo se non contiene K5 o
K3,3 .
La dimostrazione di questo importante risultato esula dagli scopi di queste note. Il
lettore interessato potrà trovare una completa trattazione su [5].
4.3
Esercizi
Es. 4.3.1 Dato K2,4 dire se esiste un possibile embedding planare. Se si, disegnarlo e
costruire il suo grafo duale.
Grafi Planari
59
Es. 4.3.2 Dato il grafo in figura, disegnare il suo grafo duale. Dire quante sono le facce e
validarlo con la legge di Eulero.
Es. 4.3.3 A partire dal grafo dell’esercizio precedente, connettere il grafo con un singolo
bridge e quindi costruire il duale. Dire quante sono le facce e applicare la legge di Eulero.
Es. 4.3.4 Fornire un esempio di un grafo in grado di dimostrare che G∗ è non isomorfo
a G.
Es. 4.3.5 E’ possibile disegnare un grafo planare semplice con 3 vertici e 4 facce? Perché?
Se è possibile, disegnarlo.
Es. 4.3.6 E’ possibile disegnare un grafo planare semplice con 4 vertici e 4 facce? Perché?
Se è possibile, disegnarlo.
Es. 4.3.7 E’ possibile disegnare un grafo planare semplice con 6 facce e 5 vertici? Se si,
disegnarlo.
Es. 4.3.8 E’ sempre possibile disegnare un grafo planare con n > 2 vertici e 3n − 6 archi?
Es. 4.3.9 Dato un grafo semplice e connesso, una condizione necessaria per la planarità
è che sia m ≤ 3n − 6. É possibile disegnare un grafo planare di 11 nodi in cui ogni nodo
ha grado almeno pari a 5? Perché?
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.
60
Algoritmi su grafo
61
Partire dalla sorgente ed identificare un numero via via crescente di nodi raggiungibili 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
62
Algoritmi su grafo
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
quindi termina al più in O(m) volte.
P
i∈V
|A(i)| = m archi e
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à.
63
Algoritmi 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).
Algoritmi su grafo
64
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.
Algoritmi su grafo
65
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}.
Algoritmi su grafo
66
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.
67
Algoritmi 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,
68
Algoritmi su grafo
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.
Algoritmi su grafo
69
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.
70
Algoritmi su grafo
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.
Algoritmi su grafo
71
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
Algoritmi su grafo
72
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-
Algoritmi su grafo
73
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 = ∅;
74
Algoritmi su grafo
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
75
Algoritmi 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).
Algoritmi su grafo
76
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 ⋆ .
77
Algoritmi 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.
Algoritmi su grafo
5.4
78
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.
Capitolo 6
Problemi di flusso su grafo
In questo capitolo introdurremo ed analizzeremo il seguente problema: si vuole trasferire,
a minimo costo, un flusso di beni attraverso una rete in modo da soddisfare la domanda
di certi nodi sulla base di quanto procurato da parte di altri nodi.
Sia G = (V, E) un grafo orientato al quale considereremo associato ad ogni arco e ad
ogni nodo un insieme di grandezze che li caratterizzano. In particolare, assoceremo ad ogni
arco (i, j) ∈ E un costo cij che definisce il costo per unità di flusso nell’arco stesso e che
varia linearmente con il flusso; una capacità uij che indichi la massima quantità di flusso
che può scorrere lungo l’arco; infine, un limite inferiore lij che indica la minima quantità
di flusso che deve scorrere nell’arco medesimo. Ad ogni nodo i ∈ V , invece, assoceremo una
grandezza b(i) che indicherà se il nodo è un punto di rifornimento del flusso oppure è un
punto di domanda del flusso; se b(i) > 0 allora il nodo i sarà un nodo di rifornimento del
flusso, se b(i) < 0 allora il nodo i sarà un nodo di domanda del flusso e se b(i) = 0 allora il
P
nodo i sarà un nodo di trasferimento del flusso. In generale, considereremo ∀i∈V b(i) = 0.
Per formulare il nostro problema in termini di modello di ottimizzazione, introduciamo
la variabile decisionale xij che rappresenta la quantità di flusso che scorre nell’arco (i, j).
Il Problema di flusso a costo minimo si può formulare nel seguente modo:
min
X
(i,j)∈E
79
cij xij
80
Problemi di flusso su grafo
soggetto ai vincoli:
X
j:(i,j)∈E
xij −
X
xji = b(i)
j:(j,i)∈E
lij ≤ xij ≤ uij
∀i ∈ V
(6.1)
∀(i, j) ∈ E
(6.2)
Il vincolo 6.1 impone sia soddisfatto il bilancio di massa ad ogni nodo. Infatti, esso
indica che in un nodo la quantità di flusso entrante meno la quantità di flusso uscente deve
eguagliare la quantità di flusso rifornita o richiesta. Il vincolo 6.2 impone invece che siano
rispettati i limiti inferiori e superiori della capacità di ogni arco, cioè che il flusso che scorre
rispetti lo spettro di valori di capacità ammessi. In molte applicazioni si ha per ogni arco
che lij = 0, quindi nel seguito considereremo tale valore, se non altrimenti specificato.
6.1
Algoritmi di ricerca del cammino minimo su di un
grafo
Gli algoritmi di ricerca del cammino minimo su un grafo costituiscono uno degli argomenti
principali dello studio delle reti di flusso. Le ragioni risiedono nel gran numero di applicazioni reali che possono essere risolte mediante questo modello, nella grande semplicità
nell’ottenere soluzioni in modo molto efficiente, nel fatto che, nonostante la loro semplicità,
catturano alcuni aspetti teorici che sono alla base delle reti di flusso e della teoria dei grafi
e nel fatto che compaiono come sottoproblemi in molte applicazioni.
Consideriamo un grafo orientato G = (V, E) al quale associamo ad ogni arco (i, j) ∈ E
una lunghezza cij . Il grafo ha un nodo particolare che chiameremo sorgente s. Definiamo
la lunghezza di un cammino orientato nel grafo come la somma delle lunghezze degli archi
nel cammino. Il problema del cammino minimo in un grafo G = (V, E) (in inglese,
shortest path problem, SPP) consiste nel determinare i cammini di lunghezza minima dalla
sorgente s verso ogni altro nodo in V . In termini di flusso, possiamo modellare il problema
81
Problemi di flusso su grafo
supponendo di inviare, a costo minimo, una unità di flusso verso ogni nodo i ∈ V − {s} in
un grafo senza vincoli di capacità. In termini di formulazione matematica, il modello è il
seguente:
min
X
cij xij
(i,j)∈E
soggetto ai vincoli:
X
xij −
X
xij −
j:(i,j)∈E
j:(i,j)∈E
X
xji = n − 1
i=s
(6.3)
X
xji = −1
∀i ∈ V − {s}
(6.4)
xij ≥ 0
∀(i, j) ∈ E
(6.5)
j:(j,i)∈E
j:(j,i)∈E
Nello studio del cammino minimo dobbiamo fare alcune assunzioni:
• Gli archi devono avere lunghezza intera. Questa assunzione non è restrittiva perché
nella rappresentazione nei computer i numeri irrazionali sono convertiti in numeri
razionali e qualunque numero razionale può essere trasformato in un numero intero
moltiplicandolo per un numero sufficientemente grande.
• Il grafo contiene un cammino orientato dalla sorgente s verso qualunque altro nodo
nel grafo.
• Il grafo non contiene cicli di lunghezza negativa. Per la formulazione vista, l’esistenza
di un ciclo W a costo negativo implicherebbe una soluzione non limitata inferiormente
perché potremmo inviare una quantità infinita di flusso lungo W .
Dato che esiste un cammino orientato dalla sorgente ad ogni altro nodo del grafo, allora
possiamo dire che esiste un albero dei cammini minimi che dalla sorgente si emana verso
tutti gli altri nodi i cui archi sono gli archi dei diversi cammini. L’esistenza di tale albero
è sostenuta dalla seguente proprietà:
82
Problemi di flusso su grafo
P1
P3
p
s
k
P2
Figura 6.1: Cammini da s a k.
Proprietà 6.1.1 Se il cammino s = n1 , n2 , . . . , nk = k è un cammino minimo dalla sorgente s al nodo k, allora per ogni p = 2, 3, . . . , k − 1, il sottocammino s = n1 , n2 , . . . , np è
il cammino minimo dalla sorgente s al nodo p.
Dimostrazione: Questa proprietà è facile da dimostrare. Infatti, se prendiamo in Figura 6.1 il cammino minimo P1 -P3 da s a k che passa attraverso un certo nodo p, allora il
cammino P2 non è il cammino minimo da s a p. Se P2 fosse il cammino minimo, allora
sarebbe sufficiente prendere il cammino P2 -P3 da s a k per avere un cammino più corto di
P1 -P3 , contraddicendo la sua ottimalità.
Indichiamo ora con d(i) la distanza del nodo i ∈ V da s. La Proprietà 6.1.1 implica
che se P è un cammino minimo da s a k, allora d(j) = d(i) + cij , ∀(i, j) ∈ P ; anche il
viceversa è vero, infatti:
Proprietà 6.1.2 Se d(j) = d(i)+cij , ∀(i, j) ∈ P , allora P deve essere il cammino minimo
tra s e k.
Dimostrazione:
Per dimostrare questa affermazione,
supponiamo che s
=
n1 , n2 , . . . , nk = k sia la successione di nodi nel cammino P . Allora, prendendo la distanza
d(k) ed aggiungendo e togliendo la distanza di ogni nodo in P otteniamo, manipolando
algebricamente l’espressione, che:
d(k) = d(nk ) = (d(nk ) − d(nk−1 )) + (d(nk−1 ) − d(nk−2 )) + . . . + (d(n2 ) − d(n1 ))
Considerando che d(n1 ) = d(s) = 0 e che per assunzione d(j) − d(i) = cij , abbiamo che:
d(k) = cnk−1 nk + cnk−1 nk−2 + . . . + cn1 n2 =
X
∀(i,j)∈P
cij
Problemi di flusso su grafo
83
Di conseguenza, P è un cammino diretto dalla sorgente s a k di lunghezza d(k) e dato
che per assunzione d(k) è la distanza del cammino minimo del nodo k, allora P deve essere
il cammino minimo del nodo k.
Quanto detto si può riassumere nel seguente risultato:
Proprietà 6.1.3 Un cammino diretto P dalla sorgente s al nodo k è un cammino di
lunghezza minima se e solo se d(j) = d(i) + cij , ∀(i, j) ∈ P .
6.1.1
L’algoritmo di Dijkstra
L’algoritmo di Dijkstra è un algoritmo in grado di risolvere il problema della ricerca del
cammino minimo dalla sorgente s a tutti i nodi. L’algoritmo mantiene una etichetta d(i)
ai nodi che rappresentano un upper bound sulla lunghezza del cammino minimo del nodo i.
Ad ogni passo l’algoritmo partiziona i nodi in V in due insiemi: l’insieme dei nodi etichettati permanentemente e l’insieme dei nodi che sono ancora etichettati temporaneamente.
La distanza dei nodi etichettati permanentemente rappresenta la distanza del cammino
minimo dalla sorgente a tali nodi, mentre le etichette temporanee contengono un valore
che può essere maggiore o uguale alla lunghezza del cammino minimo.
L’idea di base dell’algoritmo è quella di partire dalla sorgente e cercare di etichettare
permanentemente i nodi successori. All’inizio, l’algoritmo pone il valore della distanza
della sorgente a zero ed inizializza le altre distanze ad un valore arbitrariamente alto (per
convenzione, porremo come valore iniziale delle distanze d(i) = +∞, ∀i ∈ V ). Ad ogni
iterazione, l’etichetta del nodo i è il valore della distanza minima lungo un cammino dalla sorgente che contiene, a parte i, solo nodi etichettati permanentemente. L’algoritmo
seleziona il nodo la cui etichetta ha il valore più basso tra quelli etichettati temporaneamente, lo etichetta permanentemente ed aggiorna tutte le etichette dei nodi a lui adiacenti.
L’algoritmo termina quando tutti i nodi sono stati etichettati permanentemente.
In Figura 6.2 è riportata la descrizione dell’Algoritmo di Dijkstra. Le operazioni compiute dall’algoritmo sono fondamentalmente due: una operazione di selezione del nodo ed
una operazione di aggiornamento delle distanze. La prima seleziona ad ogni passo il nodo
84
Problemi di flusso su grafo
con il valore dell’etichetta più basso, l’altra verifica la condizione d(j) > d(i) + cij e, in
caso positivo, aggiorna il valore dell’etichetta ponendo d(j) = d(i) + cij .
algorithm DIJKSTRA;
begin
S = ∅;
S =V;
∀i ∈ V , d(i) = ∞;
d(s) = 0;
pred(s) = 0;
while |S| < n do
begin
sia i ∈ S un nodo per cui d(i) = min{d(j) : j ∈ S};
S = S ∪ {i};
S = S − {i};
for ∀(i, j) ∈ A(i) do
if d(j) > d(i) + cij then
begin
d(j) = d(i) + cij ;
pred(j) = i;
end;
end;
end;
Figura 6.2: L’algoritmo di Dijkstra
Vediamo un esempio per analizzare i passi effettuati dall’algoritmo.
Esempio 6.1.1 Dato il grafo G in Figura 6.3, calcolare i cammini minimi del nodo sorgente {1} verso gli altri nodi utilizzando l’algoritmo di Dijkstra. Usare le tabelle di Etichetta
nodo, Archi e Predecessore per indicare rispettivamente l’aggiornamento delle etichette dei
nodi, gli archi candidati per ogni nodo ed i predecessori lungo il cammino minimo.
All’inizio, l’algoritmo partiziona i nodi in due insiemi S (nodi etichettati permanentemente) e S (nodi etichettati temporaneamente), pone l’etichetta della sorgente uguale a zero e
le etichette degli altri nodi uguali a ∞. Successivamente entra nel ciclo while ed esegue i
seguenti passi:
• seleziona il primo nodo ad etichetta minima, cioè la sorgente s; quindi, per ogni suo
nodo adiacente ad esso, verifica se d(2) > d(1) + c12 , d(3) > d(1) + c13 e d(4) >
85
Problemi di flusso su grafo
2
2
2
4
1
2
4
3
1
3
1
4
5
2
6
1
4
2
5
8
3
7
Figura 6.3: Grafo per l’Esempio 6.1.1 e corrispondente tabella degli archi.
d(1) + c14 ; dato che ∞ > 0 + 2 = 2, ∞ > 0 + 4 = 4 e ∞ > 0 + 2 = 2, tutte e tre le
condizioni sono verificate e quindi aggiorno le etichette ed i predecessori nel seguente
modo: d(2) = d(1) + c12 = 0 + 2 = 2 e pred(2) = 1, d(3) = d(1) + c13 = 0 + 4 = 4 e
pred(3) = 1 ed infine d(4) = d(1) + c14 = 0 + 2 = 2 e pred(4) = 1.
• al secondo passo, l’etichetta con il valore più basso è d(2) e quindi seleziono il nodo 2
che diventa etichettato permanentemente. Per i suoi nodi adiacenti si deve verificare
se d(3) > d(2) + c23 e d(5) > d(2) + c25 ; dato che solo la seconda viene verificata
(perché d(3) = 4 > d(2) + c23 = 2 + 2 = 4 non è verificata, mentre è vero che
d(5) = ∞ > d(2) + c25 = 2 + 4 = 6), aggiorno la sua etichetta ponendo d(5) =
d(2) + c25 = 2 + 4 = 6 e pred(5) = 2.
• al terzo passo, l’etichetta di valore più basso è d(4) e quindi seleziono il nodo 4 che
diventa etichettato permanentemente. Per i suoi nodi adiacenti si deve verificare se
d(3) > d(4) + c43 e d(7) > d(4) + c47 ; dato che entrambe sono verificate (perché
d(3) = 4 > d(4) + c43 = 2 + 1 = 3 e d(7) = ∞ > d(4) + c47 = 2 + 5 = 7), aggiorno le
etichette ed ottengo d(3) = 3 e pred(3) = 4, e d(7) = 7 e pred(7) = 4.
• al quarto passo, l’etichetta di valore più basso è d(3) e quindi seleziono il nodo 3 che
diventa etichettato permanentemente. Per i suoi nodi adiacenti si deve verificare se
d(5) > d(3) + c35 e d(6) > d(3) + c36 ; dato che è verificata solo la seconda, aggiorno
l’etichetta corrispondente ed ottengo d(6) = 4 e pred(6) = 3.
Problemi di flusso su grafo
86
• al quinto passo, l’etichetta di valore più basso è d(6) e quindi seleziono il nodo 6 che
diventa etichettato permanentemente. Per i suoi nodi adiacenti si deve verificare se
d(7) > d(6) + c68 e d(8) > d(6) + c68 ; dato che entrambe sono verificate, aggiorno le
due etichette ed ottengo d(7) = 6 e pred(7) = 6, e d(8) = 8 e pred(8) = 6.
• al sesto passo, l’etichetta di valore più basso è d(5) e quindi seleziono il nodo 5 che
diventa etichettato permanentemente. Per i suoi nodi adiacenti si deve verificare se
d(6) > d(5) + c56 e d(8) > d(5) + c58 ; dato che solo la seconda è verificata, aggiorno
l’etichetta corrispondente ed ottengo d(8) = 7 e pred(8) = 5.
• al settimo passo, l’etichetta di valore più basso è d(7) e quindi seleziono il nodo 7
che diventa etichettato permanentemente. Per i suoi nodi adiacenti si deve verificare se d(8) > d(7) + c78 ; dato che la relazione non è verificata non effettuo alcun
aggiornamento.
• all’ottavo ed ultimo passo, l’etichetta di valore più basso è d(8) e quindi seleziono il
nodo 8 che diventa etichettato permanentemente. Il nodo non ha nessun adiacente,
quindi l’algoritmo termina.
Nelle due tabelle in Figura 6.4 sono riassunti rispettivamente l’andamento delle etichette
lungo i singoli passi dell’algoritmo e i predecessori dei singoli nodi per ricostruire i cammini
minimi.
Correttezza dell’algoritmo di Dijkstra
Per dimostrare la correttezza dell’algoritmo di Dijkstra facciamo due ipotesi induttive. La
prima è che le etichette di distanza dei nodi in S sono ottime, la seconda che le etichette di
distanza dei nodi sono la lunghezza del cammino minimo dalla sorgente s che contengono
solo nodi in S. Per dimostrare la prima ipotesi, ricordiamoci che ad ogni passo l’algoritmo
trasferisce un nodo i con l’etichetta più piccola da S a S; quindi, occorre dimostrare che
d(i) è ottima. Ma per le ipotesi induttive poste, d(i) è la lunghezza del cammino minimo
del nodo i tra tutti i cammini che non contengono un nodo di S. Supponiamo allora, per
Problemi di flusso su grafo
87
Figura 6.4: Evoluzione delle etichette dei nodi e lista di adiacenza per il grafo di Figura 6.3.
assurdo, di considerare un cammino P che contiene almeno un nodo k ∈ S, come mostrato
in Figura 6.5 e di decomporre il cammino P nei due sottocammini P1 -P2 : il sottocammino
P1 non contiene un nodo di S, ma termina in un nodo in S; quindi, per ipotesi induttiva
la lunghezza è d(k) e dato che i ha la distanza minima in S, deve risultare che d(k) ≥ d(i)
e quindi il sottocammino P1 deve avere lunghezza almeno d(i). Dato che ogni arco ha
lunghezza non negativa, la lunghezza del sottocammino P2 è non negativa e quindi la
lunghezza del cammino P è almeno d(i).
Per dimostrare che l’algoritmo verifica la seconda ipotesi induttiva, osserviamo che dopo
che è stata effettuata l’etichettatura permanente del nodo i, le etichette di alcuni nodi in
S − {i} decrescono, perché questo è interno al cammino minimo che si sta cercando per
tali nodi. Ma dopo aver permanentemente etichettato un nodo i, l’algoritmo esamina ogni
arco (i, j) ∈ A(i) e se d(j) > d(i) + cij , allora avviene l’aggiornamento d(j) = d(i) + cij ,
pred(j) = i. Quindi, dopo l’aggiornamento, per le ipotesi induttive, il cammino dalla
sorgente al nodo j definito dal vettore dei predecessori soddisfa la Proprietà 6.1.3 e quindi
le etichette di distanza di ogni nodo in S − {i} sono la lunghezza del cammino minimo
soggette alla restrizione che ogni nodo del cammino deve appartenere ad S ∪ {i}.
Per quanto riguarda la complessità computazionale dell’algoritmo di Dijkstra partiamo
dalle due operazioni di base che l’algoritmo esegue. In particolare, l’operazione di selezione
dei nodi viene eseguita n volte ed ad ogni passo scandisce ogni nodo etichettato tempora-
88
Problemi di flusso su grafo
S
S
i
pred(i)
P2
s
k
P1
Figura 6.5: Disegno per la correttezza di Dijkstra
neamente, quindi esegue n + (n − 1) + (n − 2) + . . . + 1 = O(n2 ) iterazioni. L’operazione
di aggiornamento delle etichette viene eseguita |A(i)| volte per ogni nodo i, quindi lungo
P
tutta l’esecuzione dell’algoritmo viene eseguita ∀i∈V |A(i)| = m; dato che ogni operazione di aggiornamento richiede O(1), ne segue che l’algoritmo impiega O(m) per aggior-
nare le etichette. Riassumendo, dato che O(m) < O(n2 ), la complessità computazionale
dell’algoritmo di Dijkstra è O(n2 ).
Il valore di complessità trovato è il migliore che si può ottenere per grafi molto densi,
mentre può essere migliorato nel caso di grafi sparsi. Infatti, la complessità della operazione di selezione dei nodi pesa considerevolmente di più rispetto alla complessità dell’aggiornamento delle etichette. Di conseguenza sono stati effettuati molti sforzi diretti al
miglioramento dell’efficienza di tale operazione e, in particolare, implementando strutture
dati più elaborate, si è riuscito a diminuire la complessità sino a O(m + n log n) nel caso
di liste di nodi memorizzate come liste di Fibonacci. Rimandiamo il lettore a [10] per una
trattazione completa sulle diverse implementazioni.
6.2
Problemi di massimo flusso su grafo
Il problema della ricerca del massimo flusso su di un grafo è un problema complementare a
quello della ricerca del cammino minimo. Infatti, alcuni aspetti del problema del flusso a
costo minimo sono catturati dal problema del cammino minimo nel quale sono considerati
i costi ma non le capacità; il problema del massimo flusso, invece, considera le capacità, ma
89
Problemi di flusso su grafo
non i costi. L’unione dei problemi del cammino minimo e del flusso massimo rappresenta
la combinazione di tutti gli ingredienti di base dell’ottimizzazione delle reti di flusso.
Consideriamo un grafo orientato G = (V, E) nel quale ad ogni arco (i, j) ∈ E sia
assegnata una capacità uij ≥ 0 e sia U = max{uij : uij < ∞, ∀(i, j) ∈ E}. Il Problema
del massimo flusso su di un grafo si può enunciare nel seguente modo: in un grafo
orientato con capacità, determinare il massimo flusso che può essere inviato da un nodo
s chiamato sorgente ad un nodo t chiamato pozzo, senza eccedere le capacità dei singoli
archi. Il problema può essere formulato matematicamente nel seguente modo:
max v
soggetto ai vincoli:
X
xij −
X
xij −
X
xij −
j:(i,j)∈E
j:(i,j)∈E
j:(i,j)∈E
X
xji = v
i=s
(6.6)
X
xji = 0
∀i ∈ V − {s} − {t}
(6.7)
X
xji = −v
i=t
(6.8)
∀(i, j) ∈ E
(6.9)
j:(j,i)∈E
j:(j,i)∈E
j:(j,i)∈E
0 ≤ xij ≤ uij
Il vettore x = {xij } che soddisfa le equazioni scritte conterrà il valore del flusso per
ogni arco (i, j) e la variabile v conterrà il valore totale del flusso. Nell’analisi del problema
del massimo flusso poniamo le seguenti assunzioni:
• Tutte le capacità sono interi non negativi.
• Il grafo non contiene un cammino diretto dal nodo s al nodo t composto solo da archi
di capacità infinita. Questa assunzione impedisce che una quantità di flusso infinita
possa scorrere lungo un cammino, rendendo il problema illimitato.
• Se il grafo contiene l’arco (i, j) allora contiene anche l’arco (j, i). Questa assunzione
non è restrittiva perché possiamo sempre aggiungere archi a capacità nulla.
90
Problemi di flusso su grafo
• Il grafo non contiene archi paralleli.
Per poter sviluppare l’algoritmo necessario per risolvere il nostro problema, abbiamo
bisogno di definire il concetto di grafo residuo G(x) corrispondente ad un flusso assegnato
x. Supponiamo che un arco (i, j) trasporti xij quantità di flusso. Quindi possiamo ancora
inviare sull’arco uij − xij unità di flusso dal nodo i al nodo j; analogamente, potremmo
inviare xij unità di flusso da j ad i per annullare il flusso che scorre in (i, j). Sulla base
di questa osservazione possiamo definire un grafo residuo, rispetto ad un dato flusso x,
sostituendo ogni arco (i, j) con una coppia di archi: un arco (i, j) con capacità residua
rij = uij − xij e un arco (j, i) con capacità residua rji = xji , come si può notare in
Figura 6.6. Il grafo residuo consiste solo negli archi con capacità residua positiva. Dalla
(xij , uij )
i
rij
j
i
uij − xij
j
⇒
j
i
xji
Figura 6.6: Costruzione del grafo residuo.
definizione di grafo residuo che abbiamo dato, ne segue che la capacità residua è composta
da due componenti: una componente che indica la capacità non utilizzata uij − xij ed
una componente xji che indica il flusso corrente. Quindi, possiamo scrivere che rij =
uij − xij + xji .
Nella Sezione 5.3 abbiamo dato la definizione di taglio in un grafo come la partizione
dei suoi nodi in due insiemi, S e S = V − S, usando la notazione [S, S]. Nel seguito di
questo capitolo faremo riferimento ai tagli di tipo s-t, ovvero quei tagli nei quali s ∈ S
e t ∈ S. Inoltre, definiremo come archi diretti del taglio [S, S] gli archi (i, j) tali per cui
i ∈ S e j ∈ S e con archi inversi del taglio [S, S] gli archi (i, j) tali per cui i ∈ S e j ∈ S;
indicheremo con (S, S) l’insieme degli archi diretti del taglio [S, S] e con (S, S) l’insieme
degli archi inversi del taglio [S, S]. Per esempio, in Figura 6.8 è riportato un taglio s-t
per il grafo G di Figura 6.7, dove [S, S] = {(1, 3), (3, 4), (4, 6)}, (S, S) = {(1, 3), (4, 6)} e
(S, S) = {(3, 4)}.
91
Problemi di flusso su grafo
G
2
4
(1,2)
6
(1,2)
(1,2)
(2,2)
3
2
1
(2,2)
1
1
G(x)
(1,2)
4
2
1
1
1
1
6
1
1
2
2
5
(1,3)
1
5
3
1
Figura 6.7: Un grafo G con un flusso assegnato x ed il corrispondente grafo residuo G(x).
2
4
6
1
3
5
Figura 6.8: Un taglio s-t per il grafo di Figura 6.7.
92
Problemi di flusso su grafo
Definizione 6.2.1 Si definisce capacità u[S, S] di un taglio s-t la somma delle capacità
degli archi diretti del taglio, ovvero:
X
u[S, S] =
uij
(6.10)
(i,j)∈(S,S)
Si definisce taglio minimo il taglio s-t che tra tutti i possibili tagli s-t ha capacità
minima.
Chiaramente, la capacità di un taglio è un limite superiore della quantità massima di
flusso che possiamo inviare dai nodi in S ai nodi in S rispettando i vincoli di capacità
imposti.
Definizione 6.2.2 Si definisce capacità residua r[S, S] di un taglio s-t la somma delle
capacità residue degli archi diretti del taglio, ovvero:
r[S, S] =
X
rij
(6.11)
(i,j)∈(S,S)
Dato un flusso x su di un grafo, per calcolare il flusso attraverso un taglio s-t possiamo
utilizzare il vincolo di bilancio di massa della formulazione, per cui:
v=
X X
∀i∈S j:(i,j)∈E
xij −
X
xji
j:(j,i)∈E
Questa espressione si può semplificare notando che se (p, q) ∈ E e p ∈ S e q ∈ S,
allora comparirà un xpq nella prima sommatoria all’interno delle parentesi quadre (quando
i = p), ed un −xpq nella seconda (quando j = q). Considerando che nella sommatoria non
compaiono neanche le componenti di x che fanno riferimento ad archi che hanno solo nodi
in S, possiamo scrivere che:
v=
X
(i,j)∈(S,S)
xij −
X
xij
(6.12)
(i,j)∈(S,S)
Questa relazione indica che il flusso dai nodi in S ai nodi in S è uguale al flusso che
da S va in S, meno il flusso che da S va a S e, dato che il primo membro dell’equazione è
93
Problemi di flusso su grafo
esattamente il valore del flusso, abbiamo che esso eguaglia esattamente il valore del flusso
nel taglio. Considerando che xij ≤ uij e che xij ≥ 0, possiamo scrivere che:
v≤
X
uij
(6.13)
(i,j)∈(S,S)
Questo risultato ci indica che il valore di un qualunque flusso sul grafo è minore o al più
uguale alla capacità di un su qualunque taglio s-t. Tale risultato è abbastanza intuitivo
perché ogni flusso che scorre da s a t deve attraversare ogni taglio s-t e, quindi, non ne può
eccedere la capacità.
6.2.1
L’algoritmo di Ford e Fulkerson
Per risolvere il problema del massimo flusso in un grafo attraverso l’algoritmo che stiamo
per introdurre abbiamo bisogno di alcune definizioni sul grafo residuo:
Definizione 6.2.3 Si definisce cammino aumentante nel grafo residuo un cammino
diretto dalla sorgente al pozzo e capacità residua δ la minima capacità residua di ogni
arco nel cammino aumentante.
Nel grafo in Figura 6.7, un cammino aumentante nel grafo G(x) è costituito dagli
archi {(1, 2), (2, 4), (4, 3), (3, 5), (5, 6)} e la capacità residua δ = min{r12 , r24 , r43 , r35 , r56 } =
min{1, 1, 1, 2, 1} = 1. Come si può notare, la capacità residua è sempre maggiore di zero;
quindi, non appena un grafo contiene un cammino aumentante, possiamo inviare ulteriore
flusso dalla sorgente al pozzo.
Quest’ultima osservazione ci suggerisce l’algoritmo per risolvere il problema della ricerca
del massimo flusso. Infatti, potremmo iniziare utilizzando le tecniche di ricerca viste nella
Sezione 5.1 per identificare un cammino da s a t nel grafo G(x) partizionando i nodi del
grafo in due insiemi: nodi etichettati (quelli raggiungibili da s) e nodi non etichettati
(quelli non raggiungibili da s). Se alla fine del processo t è etichettato, allora invio la
massima quantità di flusso, pari alla capacità residua sul cammino aumentante trovato;
quindi, cancello tutte le etichette e ripeto la procedura iterativamente. L’algoritmo termina
94
Problemi di flusso su grafo
quando non riesco ad etichettare t, cioè quando non esiste un cammino aumentante dalla
sorgente al pozzo.
Prima di esporre l’algoritmo, si vuole sottolineare che la ricerca di un cammino aumentante nel grafo residuo G(x) corrisponde alla ricerca in G di un cammino dalla sorgente al
pozzo, non necessariamente orientato, con xij < uij per ogni arco diretto nel verso del cammino e con xij > 0 per ogni arco inverso rispetto al verso del cammino ed esiste un cammino
aumentante rispetto ad un certo flusso x se e soltanto se esiste un cammino diretto da s a t
in G(x). Se ad un certo passo dell’algoritmo si aggiorna il flusso con un flusso addizionale
δ, allora xij varierà rispettando la definizione di capacità residua (rij = uij − xij + xji ) o
aumentando xij di δ unità o diminuendo xji di δ unità, oppure si avrà una combinazione
convessa delle due possibilità precedenti.
Le osservazioni fatte fin qui possono essere formalizzate nell’algoritmo di Ford e Fulkerson descritto in Figura 6.9. La correttezza dell’algoritmo segue dal fatto che sono possibili
due casi: o l’algoritmo trova un cammino aumentante dalla sorgente al pozzo, oppure non
riesce a trovare alcun cammino. Se si verifica il secondo caso dobbiamo dimostrare che
allora il flusso è ottimo. Supponiamo quindi che ad un certo passo, S sia l’insieme dei nodi
etichettati e S = V − S l’insieme dei nodi non etichettati, con s ∈ S e t ∈ S. Se l’algoritmo
non può etichettare i nodi in S a partire dai nodi in S, allora rij = 0, ∀(i, j) ∈ (S, S);
inoltre, dato che rij = uij − xij + xji , xij ≤ uij e xji ≥ 0, allora la condizione rij = 0
implica che xij = uij per ogni arco (i, j) ∈ (S, S) e xij = 0 per ogni arco (i, j) ∈ (S, S).
Sostituendo questi valori nell’Equazione 6.12 otteniamo:
v=
X
(i,j)∈(S,S)
xij −
X
xij =
(i,j)∈(S,S)
X
uij = u[S, S]
(6.14)
(i,j)∈(S,S)
Questa relazione mostra che il valore del flusso corrente x eguaglia la capacità del taglio
[S, S] e dato che l’Equazione 6.13 implica che x è il flusso massimo e [S, S] è il taglio minimo,
allora abbiamo dimostrato il seguente risultato:
Teorema 6.2.1 Il valore massimo del flusso dalla sorgente s al pozzo t in un grafo con
capacità eguaglia la capacità del minimo taglio s-t.
Problemi di flusso su grafo
95
algorithm FORD&FULKERSON;
begin
etichetta il nodo t;
while t è etichettato do
begin
cancella le etichette di tutte i nodi i ∈ V ;
pred(j) = 0, ∀i ∈ V ;
etichetta s e poni LISTA= {s};
while LISTA6= ∅ e t è non etichettato do
begin
rimuovi un nodo i da LISTA;
for (i, j) ∈ G(x) do
if nodo j è non etichettato then;
begin
pred(j) = i;
etichetta j;
aggiungi j a LISTA;
end;
end;
if t è etichettato then
begin
usa le etichette dei predecessori per trovare all’indietro
il cammino aumentante P da s a t;
δ = min{rij , (i, j) ∈ P };
aumenta di δ unità di flusso lungo P e aggiorna G(x);
end;
end;
end;
Figura 6.9: L’algoritmo di Ford e Fulkerson
Il teorema precedente, che chiameremo Teorema del massimo flusso e del minimo taglio ci dice anche che quando l’algoritmo di Ford e Fulkerson termina con il massimo
flusso, contemporaneamente ci fornisce anche il taglio minimo.
Esempio 6.2.1 Dato il grafo in Figura 6.10, calcolare il flusso massimo dalla sorgente
{1} al pozzo {8} utilizzando l’algoritmo di Ford e Fulkerson, disegnando la successione dei
grafi residui. Indicare il taglio minimo corrispondente al flusso trovato.
96
Problemi di flusso su grafo
2
2
4
4
2
6
1
5
3
3
3
2
3
3
2
4
5
6
8
4
7
Figura 6.10: Grafo per l’Esempio 6.2.1.
Nel grafo in Figura 6.10 il flusso è posto inizialmente a xij = 0 per ogni arco; quindi, il
primo grafo residuo coincide con il grafo di partenza.
• Nella prima iterazione l’algoritmo trova il cammino aumentante {(1, 2), (2, 5), (5, 8)},
con δ = min{r12 , r25 , r58 } = min{4, 2, 4} = 2. L’algoritmo esegue l’aumento del flusso
pari a δ = 2 unità ed aggiorna il grafo residuo, disegnato in Figura 6.11.
2
2
2
2
2
6
1
3
5
2
3
3
3
2
4
2
6
5
3
2
8
4
7
Figura 6.11: Il grafo residuo dopo la prima iterazione.
• Nella
seconda
iterazione
l’algoritmo
trova
il
cammino
{(1, 2), (2, 4), (4, 7), (7, 8)} con δ = min{r12 , r24 , r47 , r78 } = 2.
aumentante
L’algoritmo ese-
gue l’aumento del flusso pari a δ = 2 unità ed aggiorna il grafo residuo, disegnato in
Figura 6.12.
• Nella terza iterazione l’algoritmo trova il cammino aumentante {(1, 3), (3, 5), (5, 8)}
97
Problemi di flusso su grafo
2
2
4
2
2
6
1
2
4
6
3
1
2
2
3
3
2
3
5
5
8
2
2
7
Figura 6.12: Il grafo residuo dopo la seconda iterazione.
con δ = min{r13 , r35 , r58 } = 2. L’algoritmo esegue l’aumento del flusso pari a δ = 2
unità ed aggiorna il grafo residuo, disegnato in Figura 6.13.
2
2
4
4
3
3
2
3
4
2
2
1
5
2
3
1
4
6
2
5
8
2
2
7
Figura 6.13: Il grafo residuo dopo la terza iterazione.
• Nella quarta iterazione l’algoritmo trova il cammino aumentante {(1, 3), (3, 6), (6, 8)}
con δ = min{r13 , r36 , r68 } = 3. L’algoritmo esegue l’aumento del flusso pari a δ = 3
unità ed aggiorna il grafo residuo, disegnato in Figura 6.14.
• Nella quinta iterazione, l’algoritmo non riesce ad etichettare il pozzo e quindi termina.
Il flusso massimo è pari alla somma degli incrementi eseguiti nei singoli passi, cioè
v ⋆ = 2 + 2 + 2 + 3 = 9. Il taglio minimo è riportato in Figura 6.15.
98
Problemi di flusso su grafo
2
2
4
1
4
2
2
2
3
3
1
5
6
2
3
1
2
2
4
8
3
3
2
5
7
Figura 6.14: Il grafo residuo dopo la quarta iterazione.
2
2
4
3
4
2
6
1
5
3
3
3
3
2
4
6
2
5
8
4
7
Figura 6.15: Il taglio minimo del grafo di Figura 6.10.
99
Problemi di flusso su grafo
Complessità computazionale dell’algoritmo di Ford e Fulkerson
Per calcolare la complessità computazionale osserviamo che l’algoritmo esegue una ricerca
per trovare un cammino dalla sorgente al pozzo (che sappiamo essere eseguibile, dalla
Sezione 5.1, in O(m) passi) tante volte quanto è possibile eseguire degli aumenti di flusso.
Per tali aumenti, se le capacità sono intere e limitate da U , allora la capacità di un taglio
(s, N − {s}) è al più nU . Di conseguenza, siccome l’algoritmo aumenta il flusso di almeno
una unità ogni iterazione, ne segue che globalmente l’algoritmo esegue O(nmU ) iterazioni.
L’algoritmo visto è sicuramente uno dei più semplici per risolvere il problema del massimo flusso, ma il valore di complessità computazionale è legato al valore di U e questo
potrebbe portare a casi nei quali l’algoritmo non risulta essere efficiente (per esempio, se
U = 2n ). Inoltre, in alcuni casi può eseguire tante iterazioni quante indicate dal caso
peggiore. Per esempio, se si considera l’istanza della Figura 6.16, l’algoritmo potrebbe
selezionare i cammini aumentanti 1 − 3 − 2 − 4 e 1 − 2 − 3 − 4, alternativamente, 106 volte,
ogni volta aumentando il flusso di una unità.
2
2
6
6
10
10
1
106
106
3
10
4
1
106 -1
6
1
106 -1
1
1
106 -1
1
1
4
1
1
1
106
3
2
106 -1
106 -1
1
4
1
106 -1
3
Figura 6.16: Istanza patologica per l’algoritmo di Ford e Fulkerson.
Un altro difetto dell’algoritmo risiede nel fatto che ad ogni passo deve essere rieseguito il
processo di etichettatura e, quindi, le informazioni che si generano sui cammini aumentanti
vengono perse ad ogni passo e si devono ricalcolare di nuovo.
In [10] sono riportati diversi algoritmi efficienti per la risoluzione del problema del
massimo flusso che non risentono dei limiti dell’algoritmo di Ford e Fulkerson.
Problemi di flusso su grafo
6.3
Esercizi
Da inserire
100
Indice analitico
Albero, 44
Ricoprente, 45
Minimo, 69
Algoritmo, 18
Densità, 6
Distanza, 41
Embedding planare, 52
di Kruskal, 73
Faccia, 53
di ordinamento topologico, 67
Flusso, 79
di Prim, 75
Foresta, 44
di Dijkstra, 83
Formula di Eulero, 57
di Ford e Fulkerson, 93
Grafo, 3
di ricerca su grafo, 60
Aciclico, 42
in ampiezza, 63
Bipartito, 11, 43
in profondità, 65
Complemento, 9
di ricerca su stringa, 20
Completo, 6
Archi, 3
Denso, 6
Multipli, 3
Dimensione di un, 5
Disegno di un, 52
Bridge, 40
Duale, 54
Ciclo, 13
Euleriano, 45
Clique, 6
k-regolare, 8
Componenti connesse, 39
Ordine di un, 5
Connessione, 15
Orientato, 32
Crescita di funzioni, 22
Piano, 52
Combinazione di, 24
Planare, 52
Cut-vertex, 40
Semplice, 3
101
102
INDICE ANALITICO
Sparso, 6
Insieme indipendente, 9
Isomorfismo, 36
Classi di equivalenza, 39
K-Fattorizzazione, 8
Lemma Handshaking, 7
Lista di adiacenza, 36
Loop, 3
Matrice di adiacenza, 34
Matrice di incidenza, 33
Nodi, 3
Notazione Big-O, 23
Numero cromatico, 12
Ordinamento topologico, 66
Path, 13, 14
Problema
decisionale, 17
del cammino minimo, 80
del flusso massimo, 88
dell’albero ricoprente minimo, 69
di flusso a costo minimo, 79
Regione, 53
Sottografo, 4
Indotto, 4
Ricoprente, 4
Taglio, 70
Teorema
del massimo flusso e minimo taglio, 95
di Eulero, 45
di Kuratowski, 58
Trail, 14
Euleriano, 45
Hamiltoniano, 47
Vertex covering, 10
Vicinato, 7
Walk, 14
Bibliografia
[1] AA.VV. Kaliningrad Business Guide. http://guide.kaliningrad.net.
[2] B. Bollobas. Modern graph theory. Springer-Verlag, 1998.
[3] K. Steiglitz C. H. Papadimitriou.
Combinatorial optimization: algorithms and
complexity. Prentice Hall, 1982.
[4] N. Christofides. Graph theory, an algorithmic approach. Academic Press, 1975.
[5] R. Diestel. Graph Theory. Springer-Verlag, 2005.
[6] S. Fortin. The graph isomorphism problem. Technical Report TR96-20, Department
of Computer Science, The University of Alberta, Canada, July 1996.
[7] D. Jungnickel. Graphs, network and algorithms. Springer-Verlag, 1999.
[8] F. Maffioli. Elementi di programmazione matematica. Casa Editrice Ambrosiana,
2000.
[9] R. J. Wilson N. L. Biggs, E. K. Lloyd. Graph Theory 1736-1936. Oxford University
Press, 1999.
[10] J. B. Orlin R. K. Ahuja, T. L. Magnanti. Network Flows. Pearson Education, 1993.
[11] K. H. Rosen. Discrete mathematics and its applications. McGraw Hill Text, 1998.
[12] P. Serafini. Ottimizzazione. Zanichelli, 2000.
[13] A. Ventre. Introduzione ai grafi planari. Zanichelli Editore, 1983.
103
BIBLIOGRAFIA
[14] D. B. West. Introduction to graph theory. Prentice Hall, 2000.
104
Scarica