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