10th Friday Fun Session – 17th Mar 2017
Dijkstra’s algorithm is a Single-Source Shortest Path (SSSP) algorithm developed by Edsger Wybe Dijkstra. It uses a greedy process and yet finds the optimal solution. It looks similar to Breadth-first search.
Compare to Bellman-Ford
It is asymptotically the fastest SSSP algorithm, at a cost O(|E|+|V|log|V|), when min-priority queue implemented by Fibonacci heap is used.
That is quite cheap, given Bellman-Ford’s complexity of O(|V||E|) to find the same, something that can become prohibitively expensive for a dense graph having |V|2 edges.
However, while Bellman-Ford can work with negative edge and can detect negative cycle, Dijkstra’s algorithm cannot work with negative edge. Since it cannot work with negative edge, there is no question of detecting negative cycle at all.
dist //shortest path vector p //predecessor vector, used to reconstruct the path Q //vertex set for each vertex v in Graph dist[v] = ∞ p[v] = undefined add v to Q dist[s] = 0 while Q is not empty u = vertex with min dist value remove u from Q for each neighbor v of u alt = dist[u] + weight(u, v) if alt < dist[v] dist[v] = alt p[v] = u return dist, p
Given source vertex s, it finds the shortest distance from s to all other vertices. At first, it initializes dist vector to infinite to mean that it cannot reach any other vertex. And sets dist[s] = 0 to mean that it can reach itself at a cost of 0, the cheapest. All vertices including itself are added to the vertex set Q.
Then, it chooses the vertex with min dist value. At first, s (set to u) would be chosen. Then using each of the outgoing edges of u to v, it tries to minimize dist[v] by checking whether v can be reached via u using edge (u, v). If yes, dist[v] is updated. Then again it retrieves vertex u with the cheapest dist[u] value and repeats the same. This continues till Q is not empty. Whenever, a vertex u is removed from Q, it means that the shortest distance from s to u is found.
Since we are retrieving |V| vertices from Q, and for each vertex, trying with all its edges (=|V|, at max), to minimize distance to other vertices, the cost can be |V|2.
So, here we see a greedy process where it is retrieving the vertex with min dist value.
Since retrieving a vertex u from Q means that we found the minimum distance from s to u, if we are solving shortest path from a single source s to a single destination d, then when u matches the destination d, we are done and can exit.
It can also be noted that from source s, we find the shortest distances to all other vertices, in the ascending order of their distances.
Finally, we see that dist vector is continuously changing. And each time when we retrieve a vertex u, we choose the one with min dist value. That indicates using min-priority queue might be the right choice of data structure for this algorithm.
Using Fibonacci Heap
dist //shortest path vector p //predecessor vector, used to reconstruct the path Q //priority queue, implemented by Fibonacci Heap dist[s] = 0 for each vertex v if(s != v) dist[v] = ∞ p[v] = undefined Q.insert_with_priority(v, dist[v]) // insert while Q.is_empty() = false u = Q.pull_with_min_priority() // find min and delete min for each neighbor v of u alt = dist[u] + weight(u, v) if alt < dist[v] dist[v] = alt p[v] = u Q.decrease_priority(v, alt) //decrease key return dist, p
In the above algorithm, we have used a function called decrease_priority(), something that is absent in standard priority queue but present in Fibonacci heap. So the above algorithm is implemented using Fibonacci heap.
Fibonacci heap is a special implementation of a priority queue that supports decrease key (decrease_priority()) operation. Meaning, we can decrease the value of a key while it is still inside the priority queue. And this can be achieved by using constant amortized time for insert, find min and decrease key operation and log (n) time for delete min operation.
As for cost, since we have called delete operation for each of the v vertices, and we have treated each of the |E| edges once, the cost here is O(|E|+|V|log|V|), as mentioned at the beginning of this post, as the cost of Dijkstra’s algorithm.
Using Standard Priority Queue
Standard priority queue implementation takes log (n) time for both insert and delete operation and constant time for find min operation. But there is no way to change the key value (decrease key) while the item is still in the priority queue, something Dijkstra’s algorithm might need to do quite frequently as we have already seen.
If standard priority queue is used, one has to delete the item from the priority queue and then insert into it again, costing log (n) each time, or an alternative to that effect. However, as long as standard priority queue is used, it is going to be slower than Fibonacci heap. With standard priority queue, the algorithm would look like below:
dist //shortest path vector p //predecessor vector, used to reconstruct the path Q //standard priority queue for each vertex v dist[v] = ∞ p[v] = undefined dist[u] = 0 Q.insert_with_priority(u, dist[u]) // insert while Q.is_empty() = false u = Q.pull_with_min_priority() // find min and delete min for each neighbor v of u alt = dist[u] + weight(u, v) if alt < dist[v] dist[v] = alt p[v] = u insert_with_priority(v, alt) //insert v even if already exists return dist, p
There are two differences from the earlier algorithm:
First, we have not inserted all vertices into the standard priority queue at first, rather inserted the source only.
Second, instead of decreasing priority that we cannot do using standard priority queue, we have kept on inserting vertex v when dist[v] decreases. That might mean, inserting a vertex v again while it is already there inside the queue with a higher priority/dist[v]. That is another way of pushing aside the old entry (same v but with higher priority) out of consideration for the algorithm. When shortest distances from source s to all other vertices v are found, those pushed aside vertices will be pulled one by one from the priority queue and removed. They will not affect dist vector anymore. And thus the queue will be emptied and the algorithm will exit.
Please check Dijkstra’s Problem with Negative Edge for further details.