Il 0% ha trovato utile questo documento (0 voti)
8 visualizzazioni31 pagine

Algoritmi2

Il documento tratta degli algoritmi sui grafi, coprendo argomenti come la rappresentazione dei grafi, l'esplorazione tramite BFS e DFS, e concetti fondamentali come cammini, gradi dei nodi e componenti fortemente connesse. Viene fornita una panoramica delle tecniche di rappresentazione, come liste di adiacenza e matrici di adiacenza, e si discute la complessità degli algoritmi di esplorazione. Inoltre, il documento include definizioni formali e esempi pratici per illustrare i concetti trattati.

Caricato da

massdade
Copyright
© © All Rights Reserved
Per noi i diritti sui contenuti sono una cosa seria. Se sospetti che questo contenuto sia tuo, rivendicalo qui.
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd
Il 0% ha trovato utile questo documento (0 voti)
8 visualizzazioni31 pagine

Algoritmi2

Il documento tratta degli algoritmi sui grafi, coprendo argomenti come la rappresentazione dei grafi, l'esplorazione tramite BFS e DFS, e concetti fondamentali come cammini, gradi dei nodi e componenti fortemente connesse. Viene fornita una panoramica delle tecniche di rappresentazione, come liste di adiacenza e matrici di adiacenza, e si discute la complessità degli algoritmi di esplorazione. Inoltre, il documento include definizioni formali e esempi pratici per illustrare i concetti trattati.

Caricato da

massdade
Copyright
© © All Rights Reserved
Per noi i diritti sui contenuti sono una cosa seria. Se sospetti che questo contenuto sia tuo, rivendicalo qui.
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd
Sei sulla pagina 1/ 31

Algoritmi

UniVR - Dipartimento di Informatica

Fabio Irimie

Corso di Roberto Segala


2° Semestre 2024/2025
Indice
1 Grafi 2
1.1 Rappresentazione di un grafo . . . . . . . . . . . . . . . . . . . . . 3
1.2 Esplorazione di un grafo . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2.1 Visita in ampiezza (BFS: Breath First Search) . . . . . . . . 4
1.2.2 Visita in profondità (DFS: Depth First Search) . . . . . . . 7
1.3 Componenti fortemente connesse . . . . . . . . . . . . . . . . . . . 13
1.3.1 Cammini minimi . . . . . . . . . . . . . . . . . . . . . . . . 16
1.3.2 Cammini minimi con tutte le sorgenti . . . . . . . . . . . . 24
1.4 Grafo bipartito . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
1.5 Problema di flusso massimo . . . . . . . . . . . . . . . . . . . . . . 27
1.5.1 Funzione di flusso e le sue proprietà . . . . . . . . . . . . . 28
1.5.2 Rete residua . . . . . . . . . . . . . . . . . . . . . . . . . . 29

1
1 Grafi
I grafi permettono di risolvere problemi particolarmente complessi, ma la parte dif-
ficile è la conversione di un problema in un grafo. I grafi sono costituiti da nodi e
archi:

1 2

3 4

Figura 1: Esempio di grafo

• Nodi: rappresentano gli elementi del problema.


• Archi: rappresentano le relazioni tra i nodi.

I grafi in cui gli archi hanno un valore (o peso) vengono chiamati grafi pesati.
Si possono anche aggiungere delle direzioni agli archi, ottenendo così un grafo
orientato, in cui un arco si può attraversare in un solo verso.

Definizione 1.1 (Cammino). Un cammino è una sequenza di nodi per cui


esiste un arco tra ogni coppia di nodi adiacenti.

In un cammino, la ripetizione di un nodo rappresenta un loop e questo cammino


viene detto cammino ciclico. (un cammino senza cicli si dice cammino semplice)
Il grado di un nodo è il numero di archi che incidono sul nodo. Ha senso parlare
di grado di un nodo solo quando il grafo non è orientato perchè così ogni arco viene
contato una sola volta.
• Grado entrante: numero di archi entranti in un nodo.
• Grado uscente: numero di archi uscenti da un nodo.

La definizione formale di un grafo è la seguente:

Definizione 1.2. Un grafo è definito come una coppia G = (V, E) dove:

• V è un insieme di nodi.
• E è un insieme di archi:
E⊆V ×V

Dallla figura 1 si ha che:


• V = {1, 2, 3, 4}.
• E = {(1, 3), (3, 1), (1, 1), (1, 4), (4, 1), (1, 2), (2, 4), (4, 2)}.
La definizione formale dei concetti precedenti è:

2
Definizione 1.3. Il grado uscente di un nodo v in un grafo orientato G =
(V, E) è il numero di archi uscenti da v (| . . . | è la cardinalità di un insieme):

grado_uscente(v) = |{u | (v, u) ∈ E}|

Definizione 1.4. Il grado entrante di un nodo v in un grafo orientato G =


(V, E) è il numero di archi entranti in v:

grado_entrante(v) = |{u | (u, v) ∈ E}|

Definizione 1.5. Un cammino è una sequenza di nodi in cui per ogni coppia
di nodi consecutivi esiste un arco:

∀i ∈ {0 . . . n − 1} (vi , vi+1 ) ∈ E

1.1 Rappresentazione di un grafo


Per rappresentare un grafo ci sono due modi:
• Rappresentazione per liste di adiacenza: Si crea una lista in cui si rappre-
sentano i nodi e ad ogni nodo si associa la lista di tutti i nodi raggiungibili
tramite un arco. Prendiamo in considerazione la figura 1:

1 3 p 1 p 4 p 2 p /
2 4 p /
3 1 p /
4 1 p 2 p /

Figura 2: Rappresentazione per liste di adiacenza

Lo spazio in memoria occupato è Θ(|V| + |E|).


• Rappresentazione per matrice di adiacenza: Si crea una matrice A di
dimensione |V| × |V| in cui Aij = 1 se esiste un arco tra i nodi i e j, altrimenti
Aij = 0. Prendiamo in considerazione la figura 1, dove p è il peso dell’arco:

/ 1 2 3 4
1 1 1 1 1
2 0 0 0 1
3 1 0 0 0
4 1 1 0 0

Tabella 1: Rappresentazione per matrice di adiacenza

Lo spazio in memoria occupato è Θ(|V|2 ).

3
• Un grafo trasposto è un grafo in cui tutti gli archi sono invertiti.
• La chiusura transitiva di un grafo è un grafo in cui se esiste un cammino
tra due nodi allora esiste un arco diretto tra i due nodi:

a b c

Figura 3: Grafo con chiusura transitiva

• Il diametro è il percorso più lungo fra i percorsi minimi

1.2 Esplorazione di un grafo


1.2.1 Visita in ampiezza (BFS: Breath First Search)
La visita in ampiezza (o a ventaglio) è un algoritmo che permette di visitare tutti i
nodi di un grafo partendo da un nodo iniziale. L’algoritmo è il seguente:
1 // G e ’ un grafo composto da un insieme di nodi V e un insieme di
archi E
2 // s e ’ un nodo dell ’ arco
3 bfs (G , s )
4 for u in G . V
5 u . color <- white // non esplorato
6 u . distance <- + inf // distanza dal nodo s
7 u . parent <- NIL // nodo da cui si arriva a u
8
9 s . color <- gray // scoperto , ma non esplorato
10 s . distance <- 0
11 s . parent <- NIL
12 Q <- { s } // coda FIFO che contiene i nodi scoperti non esplorati
13
14 while Q ! = empty
15 u <- q . head
16
17 for v in G . adj ( u ) // lista di nodi adiacenti a u
18 if v . color = = white
19 v . color <- gray
20 v . distance <- u . distance + 1
21 v . parent <- u
22 Q . enqueue ( v )
23
24 Q . dequeue ()
25 u . color <- black // esplorato

La complessità di questo algoritmo è O(|V| + |E|).

Esempio 1.1. L’algoritmo passo per passo è il seguente, dove i colori rappre-
sentano:
• Nero: non esplorato,

• Blu: scoperto, ma non esplorato,


• Rosso: esplorato,

4
1. Primo passo:

Distanza 0
Coda s

r s t u

v w x y

2. Secondo passo:

Distanza 0 1 1
Coda s w r

r s t u

1 0

1
v w x y

3. Terzo passo:

Distanza 0 1 1 2 2 2
Coda s w
 r t x v

r s t u

1 0 2

2 1 2
v w x y

5
4. Quarto passo:

Distanza 0 1 1 2 2 2 3 3
Coda s w
 r t x v u y

r s t u

1 0 2 3

2 1 2 3
v w x y

5. Quinto passo:

Distanza 0 1 1 2 2 2 3 3
Coda s w
 r t x v u
 y

r s t u

1 0 2 3

2 1 2 3
v w x y

Se si vuole trovare il cammino minimo tra due nodi, si parte dal nodo di
destinazione e si risale al nodo di partenza seguendo il campo parent di ogni
nodo.

Questo algoritmo produce un albero dei cammini di lunghezza minima


radicato in s che ha un cammino minimo per ogni nodo, se tale cammino
esiste.

Dimostrazione: Dimostriamo che l’algoritmo BFS produce sempre un albero dei


cammini di lunghezza minima:
Sia δ(v) la lunghezza del cammino minimo da s a v. Dimostrare che
∀v v.distance = δ(v)
Per dimostrare l’uguaglianza dimostriamo che sia comtemporaneamente maggiore
e uguale e minore e uguale:

Lemma 1. ∀(u, v) ∈ E δ(v) ⩽ δ(u) + 1

6
v
δ(v)

s δ(u) + 1

δ(u)
u

Figura 4: Lemma 1

Lemma 2. ∀v v.distance ⩾ δ(v) perchè:

s.distance = 0 ⩾ 0
v.distance = u.distance + 1 ⩾ δ(u) + 1 ⩾ δ(v)

Lemma 3. Nella coda Q ci sono smpre al più 2 valori e la coda è ordinata per
distanza crescente. Sia ⟨v1 , . . . , vr ⟩ il contenuto di Q in un qualche istante, allora:

v1 .distance ⩽ v2 .distance ⩽ . . . ⩽ vr .distance ⩽ v1 .distance + 1

Questo è vero per ogni istruzione del programma, è un invariante. Ogni istruzione
che non modifica Q e non modifica le distanze non modifica l’invariante. L’inizializ-
zazione della coda e la modifica della distanza di un nodo da aggiungere alla coda
non modificano l’invariante. L’aggiunta di un nodo alla coda mantiene l’invariante.
Quindi tutte le istruzioni mantengono l’invariante.

Teorema 1.1. Sia Vk l’insieme di nodi v δ(v) = k, allora ∀v ∈ Vk esiste un


punto dell’algoritmo in cui:
• v è grigio (scoperto, ma non esplorato).
• k è assegnato a v.distance.

• se v ̸= s allora v.parent = u per qualche u ∈ Vk−1 .


• v è inserito in coda

1.2.2 Visita in profondità (DFS: Depth First Search)


L’algoritmo è il seguente:
1 // G e ’ un grafo composto da un insieme di nodi V e un insieme di
archi E
2 dfs ( G )
3 for u in G . V
4 u . color <- white // non esplorato
5 u . parent <- NIL
6
7 time <- 0
8
9 for u in G . V
10 if u . color = = white
11 dfs - visit ( u )

7
1 // Le variabili della funzione dfs sono accessibili anche da dfs -
visit
2 dfs_visit ( u )
3 u . color <- gray // scoperto , ma non esplorato
4 u . start <- time <- time + 1
5
6 for v in G . adj ( u )
7 if v . color = = white // non esplorato
8 v . parent <- u
9 dfs - visit ( v )
10
11 u . color <- black // esplorato
12 u . finish <- time <- time + 1

La complessità di questo algoritmo è O(|V| + |E|).

Esempio 1.2. I numeri a sinistra indicano il tempo di inizio della visita e i


numeri a destra la fine della visita

y z s t

3/6 2/9 1/10 11/16

Parent

4/5 7/8 12/13 14/15

x w v u

Figura 5: Visita in profondità

Riprendendo l’esempio precedente scriviamo i passaggi nel seguente modo: Il


tipo di nodo è denotato dal tipo di parentesi:
• Parentesi aperta: inizio a visitare il nodo

• Parentesi chiusa: fine della visita del nodo

Tempo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
(s (z (y (x x) y) (w w) z) s) (t (v v) (u u) t)

Questa espressione è ben parentesizzata:

8
s t
z v u
y w
x

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
(s (z (y (x x) y) (w w) z) s) (t (v v) (u u) t)

Figura 6: Visualizzazione dell’albero

Gli archi si dividono in:


• Arco dell’albero (T): è un arco che collega un nodo a un suo discendente
• Arco all’indietro (B): è un arco che collega un nodo a un suo antenato
• Arco in avanti (F): è un arco che collega un nodo a un discendente non
diretto
• Arco trasversale (C): è un arco che collega due nodi non correlati
Quindi nell’esempio precedente abbiamo:

y z s t

T T
3/6 2/9 1/10 11/16

T B T F C T T

C C
4/5 7/8 12/13 14/15

x w v u

Figura 7: Tipi di archi

nel nostro algoritmo abbiamo che il clolore degli archi distingue i vari tipi:
• Bianco (non esplorato): arco trasversale (T)
• Grigio (scoperto, ma non esplorato): arco all’indietro (B)
• Nero (esplorato): arco dell’albero o arco in avanti (F,C)
Da questo consegue che se ci sono archi all’indietro è presente un ciclo, quindi esiste
un algoritmo di complessità O(|V| + |E|) per trovare se un grafo è ciclico e questo
algoritmo è il DFS.

9
Teorema 1.2. Dopo una DFS ∀u, v gli intervalli [u.start, u.finish] sono
disgiunti, oppure uno sottointervallo dell’altro

Dimostrazione:
1. Supponiamo che u.start < v.start
(a) Se u.finish < v.start allora i due intervalli sono disgiunti
(b) Se u.start < v.finish allora v è un sottointervallo di u

Corollario: In DFS v discende da u se e solo se:

u.start < v.start < v.finish < u.finish

Teorema 1.3. Nella foresta di alberi generata da una DFS, un nodo v è un


discendente di un nodo u se e solo se al tempo u.start esiste un cammino
da u a v fatto di soli nodi bianchi (non esplorati).

Dimostrazione: Supponiamo che v discende da u, sia w un nodo del cammino


da u → v della foresta:
u.start < w.start
Quindi nel momento in cui u viene scoperto, w è ancora bianco.

• Nero: non esplorato,

• Blu: scoperto, ma non esplorato,


• Rosso: esplorato,

u w v

v raggiungibile da u al tempo u.start con cammino di nodi bianchi (non


esplorati). Supponiamo per assurdo che v non discende da u

u u1 u2 w v

Supponiamo, senza perdita di generalità, che il predecessore di v discende da


u. Sia w il predecessore di v, allora w discende da u, quindi:

w.finish < u.finish

di conseguenza:

{zv.finish} < w.finish < u.finish


u.start < w.start < |v.start <
Sottointervallo di u

10
Quindi l’intervallo v è un sottointervallo di u contraddicendo l’ipotesi e dimo-
strando che v discende da u.

Teorema 1.4. Un grafo è aciclico se e solo se DFS non trova archi indietro,
ed è ciclico se e solo se trova almeno un arco indietro.

Dimostrazione: consideriamo un qualsiasi grafo ciclico. DFS scoprirà un nodo


per primo e quel nodo lo chiamiamo u e ci sarà un nodo scoperto per ultimo
chiamato v. Se v discende da u allora esiste un cammino da u a v fatto di
nodi bianchi (non esplorati) e quindi esiste un ciclo.

Esempio 1.3. Un robot si vuole vestire e deve indossare degli indumenti in un


certo ordine. Bisogna trovare un algoritmo che trovi una soluzione tenendo in
considerazione tutti i vincoli.
Una rappresentazione più astratta del problema potrebbe essere un grafo
orientato in cui i nodi sono gli indumenti e tra due nodi c’è un arco se un
indumento deve essere indossato prima dell’altro. Ad esempio:

Slip
Orologio Calzini Camicia

Pantaloni
Scarpe Cravatta

Cintura
Giacca

Figura 8: Esempio di rappresentazione del problema

Questo grafo rappresenta una relazione di ordinamento parziale tra gli indu-
menti (se non sono presenti cicli). L’obiettivo è di prendere la relazione parziale
e renderla totale, ma mantenendola compatibile coi vincoli già imposti. In que-
sto caso ad esempio bisogna imporre altri vincoli e dire che l’orologio va messo
prima di qualcos’altro mantenendo l’ordinamento parziale dato all’inizio.
Questo problema si chiama Ordinamento topologico e la risoluzione è la
seguente:
1 // G e ’ un grafo
2 topological_sorting (G)
3 stack = dfs ( G ) // DFS ritorna una pila con i nodi in ordine
decrescente di finish

Questo algoritmo ha complessità O(|V| + |E|), quindi si può risolvere un ordi-


namento topologico in tempo lineare.

11
L’applicazione dell’algoritmo sul grafo preso in esempio è la seguente conside-
ramdo che si inizi visitando la camicia (i numeri a sinistra indicano il tempo di
inizio della visita e i numeri a destra la fine della visita):

17/18
13/14 15/16
Slip
Orologio Calzini Camicia 1/12

6/11
Pantaloni
9/10 Scarpe Cravatta 2/5

7/8 Cintura
Giacca 3/4

Figura 9: Esempio di rappresentazione del problema

Slip
Calzini
Orologio
Camicia
Pantaloni
Scarpe
Cintura
Cravatta
Giacca

Tabella 2: Stack

Per dire che l’algoritmo funziona bisogna dimostrare che:

∀(u, v) ∈ E v.finish < u.finish

Dimostrazione: Quando (u, v) viene esplorato, allora se:


• v è bianco (non esplorato) allora v è un discendente di u, quindi:

v.finish < u.finish

• v è grigio (scoperto, ma non esplorato) non è possibile, perchè se v vosse


grigio ci sarebbe un grafo ciclico e la soluzione non esiste.
• v è nero (esplorato) allora u non è ancora stato esplorato, quindi:

v.finish < u.finish

Quindi l’algoritmo funziona.

12
Teorema 1.5. In un grafo aciclico c’è per forza almeno un nodo che non ha
archi entranti.

Dimostrazione: Supponiamo per assurdo che tutti i nodi abbiano almeno un


arco entrante. Si può creare una catena di nodi di grandezza |V|+1 all’infinito,
che prima o poi si ripeterà, creando un ciclo.

Esempio 1.4. Consideriamo le strade d’Italia come grafo, dove i nodi sono le
città e gli archi sono le strade a doppio senso. Ci sono parti non raggiungibili
(come la Sardegna) e quindi ci sono più grafi separati chiamati componenti
connesse.
1 cc ( G )
2 for v in G . V
3 make_set ( v )
4 for (u , v ) in G . E
5 union (u , v )

Questo algoritmo costruisce le componenti connesse di un grafo in tempo


O(|V| + |E|). Quindi dati 2 elementi si può trovare in tempo costante se
appartengono alla stessa componente connessa e di conseguenza se sono rag-
giungibili.
Un’alternativa è modificare il DFS in modo che quando si visita un nodo si
metta un’etichetta con il numero della componente connessa.
1 dfs_cc ( G )
2 for v in G . V
3 v . color <- white
4 v . parent <- NIL
5
6 cc <- 0
7 time <- 0
8
9 for u in G . V
10 if u . color = = white
11 cc <- cc + 1
12 dfs_visit_cc (u , cc )

1 dfs_visit_cc (u , cc )
2 u . color <- gray
3 u . start <- time <- time + 1
4
5 u . cc <- cc
6
7 for v in G . adj ( u )
8 if v . color = = white
9 v . parent <- u
10 dfs_visit_cc (v , cc )
11
12 u . color <- black
13 u . finish <- time <- time + 1

1.3 Componenti fortemente connesse

13
Definizione 1.6. Dato un grafo orientato G = (V, E) una componente forte-
mente connessa (SCC) è un sottoinsieme V ′ ⊆ V tale che:

∀u, v ∈ V ′ ∃ cammino da u → v e ∃ cammino da v → u

Esempio 1.5. Nel seguente esempio ci sono 2 componenti fortemente connes-


se, una composta da a, b, c, d e l’altra da e, f, g.

b c e

d f

Figura 10: Esempio di grafo con 2 componenti fortemente connesse

L’algoritmo per individuare le componenti fortemente connesse è il seguente:


1 scc ( G )
2 finish [] = dfs_finish ( G ) // Usa DFS per trovare i tempi di fine
3 Gt = G . transpose () // Grafo trasposto
4 d f s _ f r o m _ h i g h e s t _ f i n i s h ( Gt ) // Visita i nodi in ordine
decrescente di finish

Questo algoritmo ha complessità O(|V| + |E|).

Esempio 1.6. L’algoritmo eseguito passo passo su un grafo è il seguente:

14
a b c d

13/14 11/16 1/10 8/9

12/15 3/4 2/7 5/6

e f g h

Figura 11: Tipi di archi

Mentre sul grafo trasposto:

a b c d

2/5 1/6 7/10 8/9

3/4 12/13 11/14 15/16

e f g h

Figura 12: Tipi di archi

Lemma: Dopo la prima DFS, ogni componente fortemente connessa è interamente


inclusa in un albero della foresta DFS.

Il grafo delle componenti fortemente connesse è un grafo in cui ogni nodo


corrisponde a una componente fortemente connessa e gli archi tra due nodi indicano
che esiste un arco tra due nodi delle due componenti connesse.
Se c’è un ciclo tra due componenti fortemente connesse, tutti i nodi che fanno
parte del ciclo sono in una stessa componente fortemente connessa.

Un grafo delle componenti fortemente connesse contiene un cammino infinito che


raggiunge infinite volte uno stato finale quando il cammino arriva in una componente
fortemente connessa che contiene almeno 2 nodi o un solo nodo con un autoanello.

15
1.3.1 Cammini minimi
I ponti di Verona sono tutti rotti e non si possono più riparare, ma bisogna garantire
un servizio minimo, cioè permettere ad ogni persona di spostarsi in qualunque zona
a piacimento. Quindi tra 2 zone c’è bisogno di un collegamento. Ogni ponte ha un
costo di riparazione.
L’obiettivo è quello di trovare il costo minimo per riparare i ponti in modo che
ogni zona sia raggiungibile da ogni altra zona.

I nodi saranno le zone e gli archi saranno i ponti. Sappiamo che se il numero di nodi
è n, allora il numero di ponti per garantire la connettività è n − 1. E per induzione
se si aggiunge un nodo bisogna aggiungere un arco.
1. Se si vuole minimizzare il costo, bisogna minimizzare il numero di ponti
riparati.

2. Presi due nodi esiste un unico modo per andare da un nodo all’altro. Se non
fosse così vuol dire che ci sono più ponti che collegano due nodi e quindi il
numero di ponti non è minimo. (Quindi il grafo è un albero)
3. Bisogna trovare un albero, tra tutti gli alberi che garantisce la connettività del
grafo (chiamati albero di copertura o spanning tree), con costo minimo.
Quindi bisogna trovare il Minimum Spanning Tree (MST).
4. Se i pesi negativi, allora la soluzione potrebbe non essere più un albero, però
da questa soluzione si può trovare un albero che collega tutti i nodi con
costo minimo. Quindi si può risolvere il problema in modo più generale senza
imporre i pesi positivi.
5. Si può assegnare ad ogni ponte un indice di gradimento e trovare il MST
che massimizza il gradimento. Si può usare un algoritmo che calcola il costo
minimo e invertire i segni per trovare il massimo.

L’idea dell’algoritmo è quella di tagliare in due un grafo, cioè una bipartizione


dell’insieme dei nodi. Se gli estremi di un arco fanno parte dello stesso insieme,
allora l’arco è un arco interno, altrimenti è un arco di taglio. Tra tutti gli archi
del taglio, quello con costo minimo è chiamato arco sicuro.
Lemma: Dato un albero qualsiasi con un taglio, fra tutti gli MST c’è almeno
uno che contiene l’arco sicuro.
Con questo si può ridurre il problema ad uno dello stesso tipo, ma con un nodo
in meno, e questo si può fare rappresentando due nodi collegati da un arco sicuro
come un nodo singolo e rifare un altro taglio (senza dividere ciò che è stato unito
prima).
L’algoritmo è il seguente (algoritmo di Kruskal):
1 // G e ’ un grafo
2 // w e ’ una matrice che associa ad ogni arco il suo peso
3 kruskal (G , w )
4 A <- {}
5
6 for v in G . V
7 make_set ( v )
8
9 sort ( G .E , w . no t_desc ending ) // Ordina per peso NON decrescente

16
10 for (u , v ) in G . E
11 if find_set ( u ) ! = find_set ( v )
12 A <- union (A , {( u , v ) })
13 union (u , v )
14
15 return A

Esempio 1.7. L’applicazione di questo algoritmo è la seguente:

8
b d
4 9
2
a 11 f 6
7 5
8
c e
1

Figura 13: Esempio di grafo

1. Eseguiamo il primo taglio e prendiamo il nodo sicuro:

8
b d
4 9
2
a 11 f 6
7 5
8
c e
1

2. Secondo taglio

8
b d
4 9
2
a 11 f 6
7 5
8
c e
1

3. Terzo taglio

17
8
b d
4 9
2
a 11 f 6
7 5
8
c e
1

4. Quarto taglio

8
b d
4 9
2
a 11 f 6
7 5
8
c e
1

5. Quinto taglio

8
b d
4 9
2
a 11 f 6
7 5
8
c e
1

6. Alla fine la soluzione è:

8
b d
4 9
2
a 11 f 6
7 5
8
c e
1

18
Un altro algoritmo per risolvere questo problema è l’algoritmo di Prim:
1 // G e ’ un grafo
2 // w e ’ una matrice che associa ad ogni arco il suo peso
3 // s e ’ il nodo di partenza
4 prim (G ,w , s )
5 Q <- G . V
6 for u in G . V
7 u . key <- + inf
8 s . key <- 0
9 s . parent <- NIL
10
11 while Q ! = {}
12 u <- extract_min ( Q )
13 for v in G . adj ( u )
14 if Q . contains ( v ) and w (u , v ) < v . key
15 v . parent <- u
16 v . key <- w (u , v )

La complessità di questo algoritmo è:


• V per la make_set
• E log E per l’ordinamento
• E per la find_set

• V union
Il risultato finale è:
(V + E)α(. . .) + E log E
Quindi la complessità è:
O(E log E)

Esempio 1.8. L’applicazione dell’algoritmo è la seguente


1. Il grafo è il seguente

∞ ∞
8
b d
4 9
0 ∞ 2
a 11 f 6
7 5
8
c e
1
∞ ∞

Figura 14: Esempio di grafo

2. Secondo passo

19
4 ∞
8
b d
4 9
0 ∞ 2
a 11 f 6
7 5
8
c e
1
8 ∞

Figura 15: Esempio di grafo

3. Terzo passo

4 8
8
b d
4 9
0 9 2
a 11 f 6
7 5
8
c e
1
8 ∞

Figura 16: Esempio di grafo

4. Quarto passo

4 8
8
b d
4 9
0 2 2
a 11 f 6
7 5
8
c e
1
8 6

Figura 17: Esempio di grafo

5. Quinto passo

20
4 8
8
b d
4 9
0 2 2
a 11 f 6
7 5
8
c e
1
7 5

Figura 18: Esempio di grafo

6. Ultimo passo

4 8
8
b d
4 9
0 2 2
a 11 f 6
7 5
8
c e
1
1 5

Figura 19: Esempio di grafo

Anche con questo algoritmo abbiamo trovato un MST.

Esiste un algoritmo per risolvere il problema dei cammini minimi che parte dal
presupposto che tutti gli archi non devono essere negativi:
1 // G e ’ un grafo
2 // s e ’ il nodo di partenza
3 init (G , s )
4 for v in G . V
5 v . distance <- + inf
6 v . parent <- NIL
7 s . distance <- 0
8
9 // u e v sono due nodi
10 // w rappresenta il tempo di percorrenza tra i 2 nodi
11 // Prende l ’ arco u - v che costa w e controlla se
12 // esiste un cammino minimo migliore di quello da s a v
13 // passando per u
14 relax (u , v , w )
15 if v . distance > u . distance + w (u , v )
16 v . distance <- u . distance + w (u , v )
17 v . parent <- u
18
19 // G e ’ un grafo

21
20 // s e ’ il nodo di partenza
21 // w indica la distanza tra i nodi
22 dijkstra (G ,s , w )
23 init (G , s )
24 Q <- G . V
25 while Q ! = {}
26 u <- extract_min ( Q )
27 for v in G . adj ( u )
28 relax (u ,v , w )

In questo algoritmo la relax viene effettuata una sola volta per arco perchè ogni
nodo viene estratto dalla coda una sola volta e la lista di adiacenza viene esaminata
soltanto una volta per nodo.

La complessità di questo algoritmo è


• V volte la init
• V volte la extract min
• E volte la reduce key, quindi a seconda di come viene implementata la coda
cambia la complessità dell’algoritmo
– Liste non ordinate: Complessità V per trovare il nodo minimo
∗ V 2 per la extract min, quindi la complessità diventa:

V + V 2 + E = O(V 2 )

– Liste ordinate: La extract min è costante, la reduce key è lineare


∗ V volte un lavoro costante ed E volte un lavoro linere:

EV

– Heap: La extract min è logaritmica, la reduce key è costante



(V + E) log V

Un altro algoritmo per trovare i cammini minimi è quello di Bellman-Ford:


1 bellman_ford (G , w , s )
2 init (G , s )
3 for i <- 1 to | G . V | - 1 // --
4 for (u , v ) in G . E // | VE
5 relax (u ,v , w ) // --
6
7 for (u , v ) in G . E // E
8 if v . distance > u . distance + w (u , v )
9 return false // ciclo negativo
10 return true

Se esistono archi rilassabili la soluzione non è stata trovata, se invece non esistono
archi rilassabili è stata trovata la soluzione. La complessità di questo algoritmo è:

O(VE)

Lemma: Dopo i iterazioni di Bellman-Ford, tutti i cammini minimi di lunghezza

22
al più i sono stati trovati.
Quindi l’algoritmo di Bellman-Ford trova sempre la soluzione se essa esiste.

Consideriamo un grafo che contiene un ciclo negativo.


u1 → u2 → u3 → . . . → un → u1
Supponiamo per assurdo che Bellman-Ford non dia false, sappiamo che per ognuno
dei seguenti archi vale la seguente disequazione:
u2 .distance ⩽ u1 .distance + w(u1 , u2 )
u3 .distance ⩽ u2 .distance + w(u2 , u3 )
..
.
un .distance ⩽ un−1 .distance + w(un−1 , un )
u1 .distance ⩽ un .distance + w(un , u1 )
Se sommiamo queste cose si ottiene:
n
X m
X
ui .distance ⩽ ui + costo_ciclico
i=1 i=1

n
X  X m

u
i .distance
 ⩽ ui + costo_ciclico
i=1
  i=1
Quindi otteniamo una contraddizione.

Per risolvere il problema dei cammini minimi sui grafi aciclici esiste l’algoritmo
Directed Acyclic Graph Shortest Path:
1 dag_sp (G , s , w )
2 init (G , s ) // V
3 topological_sort (G) // V + E
4 for u in G . V // - - - - - - -+
5 for v in G . adj ( u ) // -| < E | V = V + E
6 relax (u ,v , w ) // - - - - - - -+

Ogni arco viene rilassato soltanto una volta. La complessità di questo algoritmo è:
V + (V + E) + (V + E) = 3V + 2E = O(V + E)

Esercizio 1.1. In una gara vengono consegnati dei dischi con un diametro e
una morbidezz. L’obiettivo è quello di formare la pila più alta, ma un piatto
grande non può essere messo sopra un piatto piccolo e un piatto duro non deve
essere messo sopra un piatto morbido.

Il problema si può rappresentare come un grafo in cui i nodi sono i dischi e due
nodi a e b sono connessi se a può stare sotto a b. Gli archi sono orientati e il
peso è −1.

Non è definito però il nodo di partenza, quindi bisogna ridurre il problema ad


uno in cui il nodo di partenza è definito, e di conseguenza bisogna trasformare
il grafo. Si possono aggiungere due nodi che sono collegati a tutti gli altri nodi

23
con archi a peso 0 che funzionano rispettivamente da punto di partenza e da
punto di arrivo. Questi nodi sono chiamati supernodi.

Se esistessero più piatti uguali, allora essi dovrebbero essere messi tutti insieme,
quidi si aggiunge al peso il numero di piatti uguali messi nella pila. In questo
modo il cammino di costo minimo risolve il problema.

1.3.2 Cammini minimi con tutte le sorgenti


Il problema dei cammini minimi con tutte le sorgenti è la risoluzione del problema
dei cammini minimi per ogni nodo del grafo. Si avrà un vettore di parent e un
vettore di distanze che rappresentano la soluzione per ogni sorgente:
   
π1 D1
 π2   D2 
π= .  D= . 
   
 ..   .. 
πn Dn
dove π è la matrice dei parent e D è la matrice delle distanze.
La matrice D ha 0 sulla diagonale e infinito altrove:
0 ∞ ... ∞
 
∞ 0 . . . ∞
 .. .. . . .. 
 
. . . .
∞ ∞ ... 0
Per calcolare D si usa il seguente algoritmo:
1 // G e ’ un grafo
2 // w e ’ la matrice dei pesi
3 d is ta n ce _m a tr i x (G , w )
4 D <- init ( G )
5 for l <- 1 to n -1
6 D <- extend_SP (D , w )

1 // D e ’ la matrice delle distanze


2 // w e ’ la matrice dei pesi
3 extend_sp (D , w )
4 n <- rows ( D )
5 for i <- 1 to n // per ogni istanza
6 for j <- 1 to n // per ogni sorgente
7 D_1 [ i ][ j ] <- inf
8 for k <- 1 to n // per ogni destinazione
9 D_1 [ i ][ j ] <- min ( D_1 [ i ][ j ] , D [ i ][ k ] + w [ k ][ j ])

Per ogni arco si tenta di "rilassare" l’arco i → j passando per k:

j
D_1[i][j]
w[k][j]

i k
D[i][k]

L’algoritmo che moltiplica due matrici è:

24
1 mult (D , w )
2 n <- rows ( D )
3 for i <- 1 to n
4 for j <- 1 to n
5 D_1 [ i ][ j ] <- 0
6 for k <- 1 to n
7 D_1 [ i ][ j ] <- D_1 [ i ][ j ] + D [ i ][ k ] * w [ k ][ j ]

si nota che la moltiplicazione è molto simile alla extend_sp, solo che al posto
dell’operazione di sommma si fa un minimo e al posto del prodotto si fa una somma.
Quindi Bellman Ford è una moltiplicazione tra matrici, ma in un algebra diversa:
D·w
Nello specifico si sta facendo un prodotto tante volte tenendo in considerazione che
dopo aver trovato la soluzione il risultato non varia più:
I · w · w · · · w = wn−1 = wn = wn+l ∀l > 0
Questo algoritmo ha una complessità di: Θ(n4 ), però si possono sfruttare tutti
quegli algoritmi fatti per migliorare la complessità della moltiplicazione tra matrici.
Per eseguire moltiplicazioni di seguito si può sfruttare il iterative squaring:
w2 ← w · w
w4 ← w2 · w2
w8 ← w4 · w4
..
.
e questo permette di calcolare potenze pari in log(n) operazioni.

L’algoritmo di Floyd Warshal calcola gli elementi Dkij dove k è il numero di nodi
intermedi che si possono usare per calcolare il cammino minimo tra i e j:
D0 = w
La soluzione sarà quindi Dn
1 floyd_warshal (G , w )
2 n <- rows ( G )
3 D ^0 <- w
4 for k <- 1 to n
5 for i <- 1 to n
6 for j <- 1 to n
7 D ^ k [ i ][ j ] <- min ( D ^{ k -1}[ i ][ j ] , D ^{ k -1}[ i ][ k ] + D ^{ k -1}[ k ][
j ])

j
[1..k − 1]
[1..k − 1]

i k
[1..k − 1]

Se i pesi degli archi fossero tutti positivi si potrebbe applicare l’algoritmo di Dijkstra
ad ogni singola sorgente, questo algoritmo si chiama algoritmo di Johnson:

25
1 johnson (G , w )
2 for i <- 1 to n
3 D [ i ] = dijkstra (G , i , w )

e la complessità è:
V(V + E) log(V)
se il grafo è connesso si ottiene:

VE log(V)

Il problema è che questo algoritmo non si può usare su grafi con archi negativi.
Per farlo si deve trasformare il grafo in modo da non avere archi negativi e mantenere
il fatto che i cammini minimi non cambiano tra il grafo originale e quello trasformato.
Se la trasformazione ha costo maggiore dell’algoritmo da eseguire, non ha senso
farla.
I cammini minimi dipendono solo dal punto di partenza e punto di arrivo, quindi
Johnson ha definito la seguente funzione:

h : V → R ŵ(u, v) = w(u, v) + h(v) − h(u)

quindi:
k−1
X
ŵ(v0 , v1 , . . . , vk ) = ŵ(vi , vi+1 ) =
i=0

w(v0 , v1 ) + 
h(v
1 ) − h(v0 )

w(v1 , v2 ) + 
h(v
2) − h(v
1)
..
.
w(vk−1 , vk ) +  k) − 
h(v h(v )
k−1
k−1
X
= w(vi , vi+1 ) + h(vk ) − h(v0 )
i=0

Questa trasformazione produce nuovi pesi in cui i cammini minimi non cambiano.
Bisogna ora trovare la funzione h che produca pesi che non sono negativi. La
disequazione di Bellman Ford era:

v.distance ⩽ u.distance + w(u, v)

e questo diventa:

w(u, v) + u.distance − v.distance ⩾ 0

1.4 Grafo bipartito


Un grafo bipartito è un grafo in cui ogni ogni arco unisce due nodi appartenenti a
due insiemi distinti.

26
a e

b f

c g

Figura 20: Esempio di grafo bipartito

Un altro esempio sarebbe un grafo numerato in cui si dividono i numeri pari e


quelli dispari.

Se c’è una bipartizione si possono colorare diversamente i nodi appartenenti ai due


insiemi. Quindi un grafo è bipartito se e solo se è bicolorabile.
Una volta scelto il colore del primo nodo, il colore di tutti gli altri è vincolato
dalla scelta del primo nodo. Se si incontra un nodo già colorato e il colore è diverso
da quello che si aspetta, il grafo non è bipartito.

Un algoritmo per trovare se un grafo è bipartito è il seguente:


1 is_bipartito (u , color , G )
2 // DFS visit
3 u . discovered <- gray // scoperto , ma non esplorato
4 u . color <- color // colore della bipartizione
5
6 for v in G . adj ( u )
7 if v . discovered = = white // non esplorato
8 v . parent <- u
9 v . color <- ! color
10 is_bipartito (v , ! color , G )
11 else v . color = = c
12 return false
13
14 u . color <- black // esplorato
15 return true

1.5 Problema di flusso massimo

Esempio 1.9. Piove e dai monti viene giù molta acqua che finirà a valle.
Vogliamo che quest’acqua defluisca nel mare passando per una rete di canali.
L’obiettivo è sapere quanta acqua può passare per i canali senza che questi
straripino.
Si vuole trasferire un flusso materiale da un punto sorgente a un punto di
destinazione rendendo massimo il flusso.

Questo tipo di problema è rappresentabile con i grafi tramite una rete di flusso. I
nodi rappresentano i punti di partenza e arrivo del flusso, gli archi rappresentano i
canali. Per ogni canale è indicato quante unità di flusso netto passano rispetto al
totale.

27
12/12
a b

15
/1

/2
11

0
0/10
s

1/4

7/7
d

9
4/
8/

4
4/
13
11/14
c e

Figura 21: Esempio di rete di flusso

1.5.1 Funzione di flusso e le sue proprietà


La funzione di flusso è:
f:V ×V →R
e deve soddisfare:
• Il flusso di un arco non deve superare la propria capacità:
∀(u, v) f(u, v) < c(u, v)

• Anti simmetria, cioè il flusso da u a v è uguale al negativo del flusso da v a


u:
f(u, v) = −f(v, u)
• Conservazione: ciò che entra è uguale a ciò che esce, non si consuma niente
X
∀u ∈ V − {s, d} f(u, v) = 0
v
oppure X X
|f| = f(s, v) = f(u, d)
v u

Definizione utile 1.1. Il taglio di un grafo è una bipartizione (S,T) dell’in-


sieme dei nodi del grafo
S∪T =V
Ogni arco che va da un nodo in S a un nodo in T è detto arco di taglio.

Se assegnamo il nodo sorgente all’insieme s ∈ S e il nodo destinazione all’insieme


d ∈ T possiamo definire la quantità di liquido che attraversa il taglio come:
X
f(u, v)
u∈S
v∈T

Per ogni taglio che separa s e d il flusso che attraversa il taglio è sempre uguale al
modulo del flusso che esce da s: |f|
Dimostrazione:

28
• Caso base: c’è solo il nodo sorgente, quindi il flusso è la somma del flusso
uscente: X
|f| = f(s, v)
v

• Caso induttivo: si aggiunge un nuovo nodo all’interno del taglio, la variazione


di flusso per un nuovo arco all’interno del taglio è 0 perchè la somma del
flusso che entra e che esce dal nuovo arco è 0.
X
|f| = f(s, v) + 0
v

Il flusso massimo non può superare la capacità minima del taglio.

1.5.2 Rete residua


La rete residua è una rete in cui gli archi hanno una capacità residua, cioè quanto
flusso può ancora passare. La rete residua è definita come:

∀u, v cf (u, v) = c(u, v) − f(u, v)

12
a b
5

5
11

15
4

s d
11

7
5
5

4
8

3
c e
11

Figura 22: Esempio di rete residua

Si dice cammino aumentante un cammino che va da s a d in cui il flusso può


essere aumentato, e quindi il residuo diminuito. In questo cammino si dice arco
critico l’arco con capacità residua minima.

12
a b
5

5
11

15
4

s d
11

7
5
5

4
8

3
c e
11

Figura 23: Esempio di cammino aumentante

29
Quando non si trovano più cammini aumentanti si dice che il flusso è massimo.
Esiste l’algoritmo di Ford-Fulkerson che trova il flusso massimo in un grafo iterando
al massimo |f∗ |, cioè il flusso ottimo. La complessità di questo algoritmo è O(|f∗ | ·
|V + E|) e si basa sull’idea di trovare un cammino aumentante e aumentare di
una unità il flusso lungo quel cammino fino a quando non si trovano più cammini
aumentanti.

30

Potrebbero piacerti anche