dispensa - Server users.dimi.uniud.it

Transcript

dispensa - Server users.dimi.uniud.it
Cammini minimi per grafi molto grandi
1. Definizioni
È dato un grafo orientato G = (N, E). Indichiamo con n il numero di nodi N e con m il numero di archi
E. Se il problema del cammino minimo è formulato su un grafo non orientato, si può sempre trasformare il
grafo in uno orientato sostituendo l’arco non orientato (i, j) con la coppia antiparallela (i, j) e (j, i).
Per ogni arco (i, j) ∈ E è definita una lunghezza dij ≥ 0. Due nodi sono speciali e vengono detti sorgente
e destinazione, denotati rispettivamente come s e t. La lunghezza di un cammino è la somma delle lunghezze
degli archi del cammino. Il cammino minimo fra due nodi i e j è il cammino (non necessariamente unico) di
lunghezza minima fra i nodi dati. La distanza fra il nodo i e il nodo j è la lunghezza del cammino minimo
i → j.
Il problema consiste nel trovare il cammino minimo dalla sorgente alla destinazione. Si indichi con δ la
distanza da s a t, con δi la distanza da s ad i, con δ i la distanza da i ad t e con δji la distanza fra i nodi
generici i e j (nell’ordine, in generale δji 6= δij ).
In base alla proprietà di cammino minimo δji ≤ δki + δjk per ogni i, j, k ∈ N e in particolare δ ≤ δi + δ i
per ogni i ∈ N . Più in dettaglio
δ = δi + δ i
se i è su un cammino minimo,
δ < δi + δ i
se i non è su un cammino minimo
(1)
2. Complessità dell’algoritmo di Dijkstra
L’algoritmo di Dijkstra ha complessità O(n2 ) con un’implementazione semplice, O((n + m) log n) =
O(m log n) usando gli heap binari e O(m + n log n) usando gli heap di Fibonacci. L’implementazione con
gli heap binari risulta conveniente in grafi planari dove il numero di archi è lineare nel numero di nodi (più
esattamente m ≤ 3 n − 6) e quindi la complessità si abbassa a O(n log n). Per grafi molto grandi, con più di
100.000 nodi, il numero di operazioni da eseguire è comunque molto elevato. Ad esempio, con 100.000 nodi
e 300.000 archi, il numero di operazioni è (100.000 + 300.000) log2 100.000 ≈ 6.643.860, la cui esecuzione può
richiedere molti secondi.
Tuttavia si tratta di valutazioni di caso peggiore, mentre l’algoritmo di Dijkstra si comporta mediamente
in modo molto più efficiente. L’aspetto più interessante è che l’algoritmo si interrompe non appena visita il
nodo destinazione.
Ricordiamo gli aspetti salienti dell’algoritmo di Dijkstra. Ad ogni iterazione vi è un insieme di nodi
la cui distanza dalla sorgente è nota e definitiva. Indichiamo questi nodi come visitati. Inizialmente la
sorgente è l’unico nodo visitato (nella ricerca in avanti, mentre è la destinazione nella ricerca all’indietro).
I nodi adiacenti a quelli visitati ma non ancora visitati vengono detti raggiunti e per loro è disponibile una
valutazione provvisoria della distanza. Gli altri nodi sono a distanza ignota. Ad ogni iterazione un nodo
raggiunto (quello a minima distanza provvisoria) viene inserito fra i nodi visitati e nodi adiacenti a questo
1
nodo e non ancora visitati possono essere raggiunti per la prima volta oppure la loro distanza provvisoria
può essere aggiornata. L’algoritmo termina quando viene visitata la destinazione.
Se la destinazione non è molto distante dalla sorgente, solo poche operazioni possono bastare per visitare
la destinazione. Inoltre lo heap contiene soltanto i nodi finora raggiunti, che sono una frazione piccola di
tutti i nodi. Si tenga allora conto anche della distanza δ fra sorgente e destinazione per valutare meglio il
numero di operazioni. Sia n(d) il numero di nodi a distanza minore o uguale a d. Allora la complessità si
può esprimere come
O(n(δ) log n(δ))
Tipicamente, per un grafo planare, n(d) è una funzione quadratica in d. Grossolanamente si immagini
l’insieme di nodi a distanza al più d come un cerchio di raggio d centrato sulla sorgente.
Una misura dell’efficienza dell’algoritmo, applicato ad una particolare istanza, è data dal rapporto fra
il numero di nodi del cammino minimo e il numero di nodi visitati (non vengono inclusi i nodi raggiunti ma
non ancora visitati). Un’efficienza uguale ad 1 vorrebbe dire che vengono visitati solo i nodi del cammino
minimo, uno dopo l’altro fino a raggiungere la destinazione.
3. Ricerca bidirezionale
Una prima idea per ridurre il numero di operazioni consiste nel far partire contemporaneamente due
cammini minimi, uno dalla sorgente verso la destinazione e l’altro dalla destinazione verso la sorgente. Ad
un certo punto i due cammini si incontreranno mediamente a distanza δ/2 e quindi il numero di nodi che
ciascuna ricerca ha dovuto visitare è 1/4 (nell’ipotesi di funzione n(d) quadratica) di quelli che la ricerca
solo dalla sorgente avrebbe fatto. In totale quindi solo la metà dei nodi deve essere esplorata e l’efficienza è
più o meno raddoppiata.
L’algoritmo termina quando un medesimo nodo è visitato da entrambe le ricerche. Denotiamo con r
questo nodo. Sia Ss l’insieme di nodi visitato da s e S t quello visitato da t. Per definizione Ss ∩ S t = {r}.
Per ogni nodo i ∈ Ss è nota la distanza minima δi e per ogni nodo i ∈ S t è nota la distanza minima δ i .
Dimostriamo che il cammino minimo s → t deve essere fra i cammini s → i → j → t dove i ∈ Ss , j ∈ S t
ed esiste l’arco (i, j) oppure s → r → t. I cammini s → i → j → t hanno lunghezza δi + dij + δ j mentre
il cammino s → r → t ha lunghezza δr + δ r . Quindi il migliore fra questi cammini ha lunghezza minore o
uguale a δr + δ r . Ogni altro cammino deve usare un nodo k ∈
/ Ss ∪ S t . In base all’algoritmo di Dijkstra si
ha δk ≥ δr e δ k ≥ δ r e quindi δk + δ k ≥ δr + δ r . Quindi il cammino minimo è s → r → t oppure uno dei
cammini s → i → j → t. In realtà il cammino s → r → t è necessariamente già stato valutato come uno dei
cammini s → i → j → t in un passo precedente dell’algoritmo.
Quindi l’algoritmo, quando viene rilevato un arco (i, j) fra i nodi visitati rispettivamente da s e da t,
valuta la lunghezza del cammino e la confronta con quelle già calcolate.
Esempio. Si consideri un grafo a griglia (50 × 50 = 2500 nodi e 4900 archi con lunghezze generate a caso
uniformemente in {1, . . . , 5}). Il nodo in rosso è la sorgente e quello in blu la destinazione. In Figura 1(a) si
vede l’albero dei nodi esplorati nella ricerca del cammino minimo a partire dalla sorgente e in Figura 1(b)
l’albero a partire dalla destinazione.
2
Figura 1(a)
Figura 1(a)
Figura 2
Il cammino minimo ha 54 nodi e vengono visitati 2107 nodi nella ricerca in avanti e 2100 nella ricerca
all’indietro, per un’efficienza rispettivamente di 0.0256289 e 0.0257143. Come si vede viene esplorata in
entrambi i casi la quasi totalità dei nodi (84%) e quindi la possibilità di interrompere l’algoritmo non appena
si raggiunge l’altro nodo non genera un vantaggio apprezzabile. Usando la ricerca bidirezionale i due alberi
alla terminazione, quando hanno in comune esattamente un nodo, sono come in Figura 2. In questo caso
sono stati visitati 1638 nodi (il 65.5% del totale) e l’efficienza è 0.032967.
3
4. Uso di potenziali nei nodi
Guardando i nodi esplorati dall’algoritmo (Figura 2), anche con la ricerca bidirezionale, si vede che
l’algoritmo ‘perde tempo’ ad esplorare nodi che sono troppo distanti dal nodo sorgente o destinazione per
risultare utili. Naturalmente questa osservazione dipende dalla nostra conoscenza visiva del grafo mentre
l’algoritmo non ha alcuna informazione sulle distanze dei nodi ancora da esplorare. Quindi l’idea per evitare
queste perdite di tempo potrebbe essere quella di fornire tale informazione all’algoritmo. In particolare l’idea
è di poter disporre in ogni nodo di un valore che sia una limitazione inferiore alla distanza dal nodo alla
destinazione e di orientare la scelta dei nodi da esplorare in base, non solo alla distanza dalla sorgente al
nodo ma anche alla limitazione inferiore dal nodo alla destinazione.
A questo scopo, operando l’algoritmo di Dijkstra in avanti (dalla sorgente s) supponiamo di modificare
le lunghezze degli archi nel seguente modo
d0ij := dij − π i + π j
(2)
dove π i sono valori arbitrari assegnati ai nodi, detti potenziali. Si noti che i potenziali sono definiti a meno
di una costante additiva e quindi il potenziale in un nodo opportuno può essere assegnato arbitrariamente.
Dalla formulazione della programmazione dinamica all’indietro come programmazione lineare
max π s − π t
π i − π j ≤ dij
(3)
(i, j) ∈ E
si vede che i valori d0ij sono non negativi se e solo se i valori π i sono ammissibili in (3). Inoltre valori π i
ammissibili in (3) con π t ≤ 0 costituiscono limitazioni inferiori a δ i . Infatti se si sommano le diseguaglianze
π i − π j ≤ dij lungo un cammino minimo i → t, si ottiene π i − π t ≤ δ i , cioè π i ≤ δ i + π t ≤ δ i (da π t ≤ 0).
La lunghezza di un qualsiasi cammino P : s → t con le nuove distanze è
d0 (P ) =
X
d0ij =
(ij)∈P
X
(dij − π i + π j ) = d(P ) + π t − π s
(ij)∈P
Tutte le lunghezze dei cammini s → t sono quindi alterate del fattore costante π t − π s e il cammino minimo
s → t è il medesimo per qualsiasi scelta dei potenziali (inclusi potenziali nulli, cioè le lunghezze originali),
da cui
δ0 = δ + πt − πs
Quindi il cammino minimo si può calcolare con una scelta arbitraria dei potenziali purché le lunghezze d0
siano non negative, in modo da poter sempre adoperare l’algoritmo di Dijkstra. Analogamente la lunghezza
di un cammino Pk da s ad un nodo generico k vale, con le distanze d0
d0 (Pk ) =
X
(ij)∈Pk
d0ij =
X
dij − π i + π j = d(Pk ) + π k − π s
(4)
(ij)∈Pk
e quindi anche per i cammini minimi da s a k si ha
δk0 = δk + π k − π s
4
(5)
Allora l’albero dei cammini minimi da s è il medesimo per qualsiasi scelta dei potenziali. Ciò che cambia
sono le distanze sull’albero ed è questo fatto che permette di accelerare l’algoritmo con un’opportuna scelta
dei potenziali. Infatti l’algoritmo (nella ricerca in avanti) seleziona i nodi in ordine di distanza minima δk0
secondo le lunghezze d0ij , ovvero, da (5), secondo i valori δk del cammino minimo s → k con lunghezze
originali più il valore π k , stima per difetto di un cammino da k a t (il valore costante π s è irrilevante in
quanto indipendente da k).
Ad esempio, se fosse π k = δ k , cioè proprio la distanza minima da k a t anziché semplicemente una stima
per difetto, per tutti i nodi sul cammino minimo s → t si avrebbe δk0 = 0 (da π s = δ e da (1)), mentre sui
nodi non sul cammino minimo necessariamente δk0 > 0 (sempre da π s = δ e da (1)). Questo implica che
l’algoritmo sceglierà subito tutti e soli i nodi del cammino minimo.
In modo del tutto simmetrico se si opera l’algoritmo di Dijkstra a partire dalla destinazione si può
pensare di ridefinire le distanze secondo
d00ij := dij − πj + πi
per potenziali arbitrari πi (si noti la differenza con (2)). Dalla formulazione della programmazione dinamica
in avanti come programmazione lineare
max πt − πs
πj − πi ≤ dij
(i, j) ∈ E
(6)
si vede che i valori d00ij sono non negativi se e solo se i valori πi sono ammissibili in (6). Inoltre valori
πi ammissibili in (6) con πs ≤ 0 costituiscono limitazioni inferiori al cammino minimo s → i. Con le
opportune differenze valgono le stesse proprietà della formulazione in avanti, ovvero: la lunghezza di un
qualsiasi cammino P : s → t con le nuove distanze è
d00 (P ) =
X
(dij − πj + πi ) = d(P ) + πs − πt
(ij)∈P
In questo caso tutte le lunghezze dei cammini s → t sono alterate del fattore costante πs − πt , da cui
δ 00 = δ + πs − πt
La lunghezza di un cammino P k da un nodo generico k a t vale, con le distanze d00
d00 (P k ) =
X
(dij − πj + πi ) = d(P k ) + πk − πt
(ij)∈P k
e quindi
δ 00k = δ k + πk − πt
(7)
5. Algoritmo bidirezionale con potenziali
Si effettui l’algoritmo di Dijkstra sia a partire dalla sorgente con lunghezze d0 che a partire dalla destinazione con distanze d00 . I potenziali scelti per le due ricerche sono arbitrari ed in generale diversi e devono
solo essere ammissibili rispettivamente in (3) e (6) con il vincolo π t = 0 e πs = 0.
5
Come nell’algoritmo bidirezionale precedente, quando si trova un arco (i, j) con i ∈ Ss e j ∈ S t si calcola
la lunghezza vera del cammino s → i → j → t e la si confronta con quella d(P ) del migliore cammino P
trovato finora, eventualmente aggiornandola.
L’algoritmo termina non appena si visita un nodo k tale che δk + π k ≥ d(P ) nella ricerca in avanti
oppure δ k + πk ≥ d(P ) nella ricerca all’indietro. La condizione δk + π k ≥ d(P ) si può esprimere anche
come δk0 + π s ≥ d(P ) (da (5)) dove δk0 è direttamente disponibile dall’algoritmo di Dijkstra. Si noti ancora
che l’algoritmo di Dijkstra, operando con distanze d0 , visita i nodi per valori δk0 crescenti, ovvero per valori
δk + π k crescenti. Analogamente la condizione δ k + πk ≥ d(P ) si può esprimere come δ 00k + πt ≥ d(P ) (da
(7)).
Dimostriamo che alla terminazione dell’algoritmo il cammino minimo è stato trovato. Sia P il cammino
migliore fra quelli trovati con lunghezza d(P ) e sia Q un cammino generico, fra quelli non trovati, con
lunghezza d(Q). Quindi esiste un nodo h ∈ Q non visitato né dalla ricerca in avanti né da quella all’indietro.
Sia Qh la parte di cammino Q da s a h e sia Qh la parte di cammino Q da h a t e quindi d(Q) = d(Qh )+d(Qh ).
Siccome i potenziali π i sono limitazioni inferiori ai cammini minimi da i a t, cioè π i ≤ δ i , abbiamo in
particolare per il nodo h considerato
δh + π h ≤ d(Qh ) + π h ≤ d(Qh ) + δ h ≤ d(Qh ) + d(Qh ) = d(Q)
Analogamente, nell’altra direzione si ha
δ h + πh ≤ d(Qh ) + πh ≤ d(Qh ) + δh ≤ d(Qh ) + d(Qh ) = d(Q)
Sia k il nodo per il quale si verifica una delle due condizioni per la terminazione. Siccome h non è stato
visitato e l’algoritmo visita i nodi in Ss per valori δi + π i crescenti e in S t per valori δ i + πi crescenti, si ha
δk + π k ≤ δh + π h se k ∈ Ss oppure δ k + πk ≤ δ h + πh se k ∈ S t .
In entrambi i casi la condizione di terminazione implica d(P ) ≤ d(Q).
6. Scelta dei potenziali
Sono state proposte varie scelte di potenziali. Una delle più efficaci consiste nella scelta di un sottoinsieme
di nodi come di ‘landmark’ e di basare la scelta dei potenziali sulle distanze fra i landmark. Più esattamente
sia L ⊂ N un sottoinsieme sufficientemente più piccolo di N da poter calcolare e memorizzare tutte le
distanze fra un qualsiasi nodi in N e un qualsiasi nodo di L ma non tanto piccolo da rendere poco utili i
potenziali. La diseguaglianza triangolare implica
δ` ≤ δi + δ`i ,
cioè
δ` − δ`i ≤ δi
` ∈ L, i ∈ N
δi` ≤ δs` + δi ,
cioè
δi` − δs` ≤ δi
` ∈ L, i ∈ N
δ ` ≤ δi` + δ i ,
cioè
δ ` − δi` ≤ δ i
` ∈ L, i ∈ N
δ`i ≤ δ i + δ`t ,
cioè
δ`i − δ`t ≤ δ i
` ∈ L, i ∈ N
6
Quindi, dati i valori memorizzati δ`i e δi` , una limitazione inferiore di δi è data da max δ` − δ`i , δi` − δs` ,
e una limitazione inferiore di δ i è data da max δ ` − δi` , δ`i − δ`t . Una migliore limitazione inferiore si può
ottenere prendendo il massimo valore, cioè
πi = max max δ` − δ`i , δi` − δs` ,
π i = max max δ ` − δi` , δ`i − δ`t
`∈L
`∈L
(8)
Potrebbe sorgere la preoccupazione che i valori cosı̀ calcolati non soddisfino la proprietà πj − πi ≤ dij e
π i − π j ≤ dij . La proprietà è soddisfatta come si può vedere facilmente. Limitiamoci a dimostrare alcuni
casi. Sia ad esempio
π i = δ ` − δi` ,
π j = δ ` − δj`
dove ` è lo stesso landmark per entrambi i casi. Allora
π i − π j = δ ` − δi` − (δ ` − δj` ) = δj` − δi` ≤ dij
dove la diseguaglianza deriva dalla proprietà di cammino minimo ` → j. Sia invece
π j = δ`j − δ`t
π i = δ`i − δ`t ,
Allora
π i − π j = δ`i − δ`t − (δ`j − δ`t ) = δ`i − δ`j ≤ dij
dove la diseguaglianza deriva dalla proprietà di cammino minimo i → `. Consideriamo il caso di massimo
ottenuto per landmark diversi
π i = δ`i1 − δ`t1 ,
π j = δ`j2 − δ`t2 > δ`j1 − δ`t1
Allora
π i − π j = δ`i1 − δ`t1 − (δ`j2 − δ`t2 ) < δ`i1 − δ`t1 − (δ`j1 − δ`t1 ) = δ`i1 − δ`j1 ≤ dij
Gli altri casi si dimostrano in modo analogo.
Se il nodo i si trova sul cammino minimo fra s e il landmark `, allora πi è esattamente il valore di
cammino minimo s → i e, analogamente, π i è il valore di cammino minimo i → t se i si trova sul cammino
minimo fra il landmark ` e t.
Il calcolo dei valori δi` si effettua con |L| esecuzioni in avanti dell’algoritmo di Dijkstra, ognuna della
quali a partire da un landmark fino a visitare tutti i nodi. Analogamente il calcolo dei valori δ`i si effettua
con |L| esecuzioni all’indietro dell’algoritmo di Dijkstra, a partire da un landmark fino a visitare tutti i nodi.
Si tratta quindi di un calcolo che richiede un certo tempo, ma viene fatto una volta sola off-line e i suoi
dati vengono poi utilizzati in tempo costante da ogni esecuzione successiva per scelte ogni volta diverse delle
sorgenti e delle destinazioni.
Se il grafo è simmetrico basta una delle due esecuzioni perché l’esecuzione in avanti produce gli stessi
risultati dell’esecuzione all’indietro. I grafi stradali sono ‘quasi’ simmetrici e le due esecuzioni vanno fatte
entrambe. Tuttavia i valori δi` e δ`i sono molto vicini e questo fatto viene sfruttato nella memorizzazione.
Infatti la maggior parte dei bit dei due numeri sono uguali e quindi vengono memorizzati una sola volta,
mentre gli ultimi bit sono diversi e vengono memorizzati separatamente. Questo permette una compressione
7
Figura 3 – Landmark casuali
dei dati di quasi il 50%. Se il grafo ha un milione di nodi e 16 landmark (il massimo con la tecnologia attuale
se i nodi sono un milione) questa compressione permette di memorizzare i dati su una flash card e quindi
rendere più veloce la lettura dei dati.
Il calcolo dei potenziali secondo (8), che va fatto ogni qualvolta si debba calcolare un cammino minimo
fra due nuovi nodi, si può eseguire direttamente durante l’esecuzione dell’algoritmo di Dijkstra, con grande
risparmio computazionale, non appena un nodo viene raggiunto.
Presentiamo due scelte di landmark. La prima è casuale. Viene deciso il numero p di landmark e poi
vengono scelti con probabilità uniforme p nodi da N .
In Figura 3 si vede una scelta casuale di 6 landmark su 2500 nodi con i nodi visitati e il cammino
minimo. Si noti come nelle parti di grafo dove mancano landmark l’algoritmo debba visitare molti nodi. I
nodi visitati sono 206 con un’efficienza 0.262136.
Nella Figura 4 si vedono i risultati per nove scelte causali della sorgente e della destinazione, con gli
stessi landmark. I tre numeri sotto ogni figura sono il numero di nodi del cammino minimo, il numero di
nodi visitati e l’efficienza.
Il secondo modo di generare landmark cerca di distribuirli in modo uniforme sul grafo. Inizialmente
si sceglie un nodo a caso, poi si genera il nodo più distante da questo, poi quello più distante dall’insieme
generato e cosı̀ di seguito fino a generare il numero desiderato di landmark. Si possono usare sia le distanze
effettive che le distanze valutate come numero di archi. Se si usano le distanze effettive in questa fase si
possono calcolare anche il valori δ`i . Per trovare il nodo più distante dai landmark generati si può usare una
struttura a heap che contiene tutti i nodi del grafo e le distanze dall’insieme dei landmark. La radice dello
heap è il nodo più distante. Quando si preleva questo nodo (e si aggiorna lo heap) e si calcolano le distanze
da questo landmark a tutti i nodi, si aggiorna anche lo heap, nodo per nodo, diminuendo eventualmente il
valore della distanza dall’insieme.
In Figura 5 si vedono 6 landmark generati in modo equidistante (sono stati generati nel seguente ordine:
8
Fig. 5 – Landmark equidistanti, cammino minimo
prima quello casuale in alto a sinistra, poi in basso a destra, in alto a destra, in basso a sinistra, in centro
e in basso nel mezzo) e i nodi visitati per ls stessa coppia sorgente-destinazione. I nodi visitati sono 168
(contro i 54 del cammino minimo) per un’efficienza di 0.321429.
Nella Figura 6 si vedono i risultati per le stesse nove scelte causali della sorgente e della destinazione,
con gli stessi landmark.
Con tecniche di questo genere (16 landmark generati in vario modo) si riescono a calcolare con tempi
inferiori ai 200 ms i cammini minimi per grafi stradali di più di 6 milioni di nodi e 15 milioni di archi
(corrispondenti ad una parte degli Stati Uniti).
9
21, 22, 0.954545
45, 58, 0.775862
72, 318, 0.226415
32, 236, 0.135593
18, 32, 0.5625
61, 582, 0.104811
50, 436, 0.114679
35, 50, 0.7
52, 360, 0.144444
Fig. 4 – Landmark casuali
10
21, 24, 0.875
45, 58, 0.775862
72, 424, 0.169811
32, 120, 0.266667
18, 32, 0.5625
61, 108, 0.564815
50, 148, 0.337838
35, 44, 0.795455
52, 566, 0.0918728
Fig. 6 – Landmark equidistanti
11