Perché l'algoritmo di Dijkstra utilizza la chiave di diminuzione?


93

L'algoritmo di Dijkstra mi è stato insegnato era il seguente

while pqueue is not empty:
    distance, node = pqueue.delete_min()
    if node has been visited:
        continue
    else:
        mark node as visited
    if node == target:
        break
    for each neighbor of node:
         pqueue.insert(distance + distance_to_neighbor, neighbor)

Ma ho letto alcune letture riguardanti l'algoritmo e molte versioni che vedo usano il tasto di diminuzione invece di inserire.

Perché questo e quali sono le differenze tra i due approcci?


14
Downvoter- Puoi spiegare cosa c'è di sbagliato in questa domanda? Penso che sia perfettamente giusto, e molte persone (me compreso) sono state introdotte per la prima volta alla versione OP di Dijkstra piuttosto che alla versione con chiave decrescente.
templatetypedef

Risposte:


68

Il motivo per utilizzare il tasto di diminuzione anziché il reinserimento dei nodi è di mantenere basso il numero di nodi nella coda di priorità, mantenendo così basso il numero totale di code di priorità rimosse e il costo di ogni bilanciamento della coda di priorità basso.

In un'implementazione dell'algoritmo di Dijkstra che reinserisce i nodi nella coda di priorità con le loro nuove priorità, viene aggiunto un nodo alla coda di priorità per ciascuno degli m archi nel grafico. Ciò significa che ci sono operazioni di accodamento e operazioni di accodamento sulla coda di priorità, dando un tempo di esecuzione totale di O (m T e + m T d ), dove T e è il tempo richiesto per accodarsi nella coda di priorità e T d è il tempo necessario per rimuovere la coda dalla coda prioritaria.

In un'implementazione dell'algoritmo di Dijkstra che supporta la chiave di diminuzione, la coda di priorità che contiene i nodi inizia con n nodi e ad ogni passo dell'algoritmo rimuove un nodo. Ciò significa che il numero totale di heap dequeue è n. Ogni nodo avrà il tasto di diminuzione chiamato potenzialmente una volta per ogni bordo che lo conduce, quindi il numero totale di tasti di diminuzione effettuati è al massimo m. Questo dà un tempo di esecuzione di (n T e + n T d + m T k ), dove T k è il tempo richiesto per chiamare il tasto di diminuzione.

Quindi che effetto ha questo sul runtime? Dipende dalla coda di priorità che usi. Ecco una rapida tabella che mostra diverse code di priorità e i tempi di esecuzione complessivi delle diverse implementazioni dell'algoritmo di Dijkstra:

Queue          |  T_e   |  T_d   |  T_k   | w/o Dec-Key |   w/Dec-Key
---------------+--------+--------+--------+-------------+---------------
Binary Heap    |O(log N)|O(log N)|O(log N)| O(M log N)  |   O(M log N)
Binomial Heap  |O(log N)|O(log N)|O(log N)| O(M log N)  |   O(M log N)
Fibonacci Heap |  O(1)  |O(log N)|  O(1)  | O(M log N)  | O(M + N log N)

Come puoi vedere, con la maggior parte dei tipi di code prioritarie, non c'è davvero una differenza nel runtime asintotico e la versione con chiave di diminuzione non è probabile che faccia molto meglio. Tuttavia, se si utilizza un'implementazione dell'heap di Fibonacci della coda di priorità, l'algoritmo di Dijkstra sarà infatti asintoticamente più efficiente quando si utilizza il tasto di diminuzione.

In breve, l'uso del tasto di diminuzione, più una buona coda di priorità, può far cadere il runtime asintotico di Dijkstra oltre ciò che è possibile se continui a fare accodamenti e rimosse dalla coda.

Oltre a questo punto, alcuni algoritmi più avanzati, come l'algoritmo dei percorsi più brevi di Gabow, utilizzano l'algoritmo di Dijkstra come una subroutine e fanno molto affidamento sull'implementazione del tasto di diminuzione. Usano il fatto che se conosci in anticipo l'intervallo di distanze valide, puoi costruire una coda di priorità super efficiente basata su questo fatto.

Spero che questo ti aiuti!


1
+1: mi ero dimenticato di tenere conto dell'heap. Un cavillo, dal momento che l'heap della versione insert ha un nodo per bordo, cioè O (m), i suoi tempi di accesso non dovrebbero essere O (log m), dando un tempo di esecuzione totale di O (m log m)? Voglio dire, in un grafo normale m non è maggiore di n ^ 2, quindi questo si riduce a O (m log n), ma in un grafico in cui due nodi possono essere uniti da più spigoli di pesi diversi, m è illimitato (ovviamente , possiamo affermare che il percorso minimo tra due nodi utilizza solo bordi minimi e ridurlo a un grafo normale, ma per il nonce, è interessante).
rampion

2
@ rampion- Hai ragione, ma poiché penso che generalmente si presume che i bordi paralleli siano stati ridotti prima di attivare l'algoritmo, non penso che O (log n) rispetto a O (log m) avrà molta importanza. Di solito si presume che m sia O (n ^ 2).
templatetypedef

27

Nel 2007 è stato pubblicato un documento che ha studiato le differenze nei tempi di esecuzione tra l'utilizzo della versione con tasto decrescente e la versione inserto. Vedi http://www.cs.utexas.edu/users/shaikat/papers/TR-07-54.pdf

La loro conclusione di base era di non utilizzare il tasto di diminuzione per la maggior parte dei grafici. Soprattutto per i grafici sparsi, la chiave di non diminuzione è significativamente più veloce della versione con chiave di diminuzione. Vedere il documento per maggiori dettagli.


7
cs.sunysb.edu/~rezaul/papers/TR-07-54.pdf è un collegamento funzionante per quel documento.
eleanora

ATTENZIONE: c'è un bug nel documento collegato. Pagina 16, funzione B.2: if k < d[u]dovrebbe essere if k <= d[u].
Xeverous

2

Ci sono due modi per implementare Dijkstra: uno usa un heap che supporta la chiave di diminuzione e un altro un heap che non lo supporta.

Sono entrambi validi in generale, ma quest'ultimo è solitamente preferito. Di seguito userò 'm' per denotare il numero di bordi e 'n' per denotare il numero di vertici del nostro grafico:

Se vuoi la migliore complessità possibile nel caso peggiore, scegli un mucchio di Fibonacci che supporta il tasto di diminuzione: otterrai un bel O (m + nlogn).

Se ti interessa il caso medio, potresti usare anche un heap binario: otterrai O (m + nlog (m / n) logn). La prova è qui , pagine 99/100. Se il grafico è denso (m >> n), sia questo che il precedente tendono a O (m).

Se vuoi sapere cosa succede se li esegui su grafici reali, potresti controllare questo documento, come ha suggerito Mark Meketon nella sua risposta.

Quello che mostreranno i risultati degli esperimenti è che un mucchio "più semplice" darà i migliori risultati nella maggior parte dei casi.

Infatti, tra le implementazioni che utilizzano un tasto di diminuzione, Dijkstra si comporta meglio quando si utilizza un semplice heap binario o un heap di accoppiamento rispetto a quando utilizza un heap di Fibonacci. Questo perché gli heap di Fibonacci coinvolgono fattori costanti più grandi e il numero effettivo di operazioni con i tasti di diminuzione tende ad essere molto più piccolo di quanto previsto nel caso peggiore.

Per ragioni simili, un heap che non deve supportare un'operazione con tasto di diminuzione, ha fattori ancora meno costanti e in realtà funziona meglio. Soprattutto se il grafico è scarso.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.