Algoritmi2
Algoritmi2
Fabio Irimie
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
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.
• V è un insieme di nodi.
• E è un insieme di archi:
E⊆V ×V
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):
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 3 p 1 p 4 p 2 p /
2 4 p /
3 1 p /
4 1 p 2 p /
/ 1 2 3 4
1 1 1 1 1
2 0 0 0 1
3 1 0 0 0
4 1 1 0 0
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
Esempio 1.1. L’algoritmo passo per passo è il seguente, dove i colori rappre-
sentano:
• Nero: non 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.
6
v
δ(v)
s δ(u) + 1
δ(u)
u
Figura 4: Lemma 1
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:
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.
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
y z s t
Parent
x w v u
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)
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)
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
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
u w v
u u1 u2 w v
di conseguenza:
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.
Slip
Orologio Calzini Camicia
Pantaloni
Scarpe Cravatta
Cintura
Giacca
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
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
Slip
Calzini
Orologio
Camicia
Pantaloni
Scarpe
Cintura
Cravatta
Giacca
Tabella 2: Stack
12
Teorema 1.5. In un grafo aciclico c’è per forza almeno un nodo che non ha
archi entranti.
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 )
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
13
Definizione 1.6. Dato un grafo orientato G = (V, E) una componente forte-
mente connessa (SCC) è un sottoinsieme V ′ ⊆ V tale che:
b c e
d f
14
a b c d
e f g h
a b c d
e f g h
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.
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
8
b d
4 9
2
a 11 f 6
7 5
8
c e
1
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
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 )
• V union
Il risultato finale è:
(V + E)α(. . .) + E log E
Quindi la complessità è:
O(E log E)
∞ ∞
8
b d
4 9
0 ∞ 2
a 11 f 6
7 5
8
c e
1
∞ ∞
2. Secondo passo
19
4 ∞
8
b d
4 9
0 ∞ 2
a 11 f 6
7 5
8
c e
1
8 ∞
3. Terzo passo
4 8
8
b d
4 9
0 9 2
a 11 f 6
7 5
8
c e
1
8 ∞
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
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
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
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.
V + V 2 + E = O(V 2 )
EV
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)
22
al più i sono stati trovati.
Quindi l’algoritmo di Bellman-Ford trova sempre la soluzione se essa esiste.
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.
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.
j
D_1[i][j]
w[k][j]
i k
D[i][k]
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:
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:
e questo diventa:
26
a e
b f
c g
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
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
12
a b
5
5
11
15
4
s d
11
7
5
5
4
8
3
c e
11
12
a b
5
5
11
15
4
s d
11
7
5
5
4
8
3
c e
11
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