Dijkstra's shortest path algorithm, is a greedy algorithm that efficiently finds shortest paths in a graph. It is named after Edsger Dijkstra, also known as a pioneer in methods for formally reasoning about programs. (About pronunciation: "Dijkstra" is Dutch and starts out sounding like "dike").
Because graphs are able to represent many things, many problems can be cast as shortest-path problems, making this algorithm a powerful and general tool. For example, Dijkstra's algorithm is a good way to implement a service like MapQuest, which finds the shortest way to drive between two points on the map. It can also be used to solve problems like network routing, where the goal is to find the shortest path for data packets to take through a switching network. It is also used in more general search algorithms for a variety of problems ranging from automated circuit layout to speech recognition.
Recall that a graph is composed of vertices (a.k.a. nodes) and edges. The shortest path problem is defined on weighted, directed graphs in which each edge has both a weight and a direction. We can think of the weight of an edge as the distance one must travel when going along that edge. For many routing problems, two vertices will be connected by a pair of edges, one going in each direction.
For example, in the following weighted directed graph, we might want to find the shortest distance and path to Pittsburgh from Ithaca. The cities are vertices and the arrows are the edges, weighted with a distance.
A path through the graph is a sequence (v1, ..., vn) such that the graph contains an edge e1 going from v1 to v2, an edge e2 going from v2 to v3, and so on. That is, all the edges must be traversed in the forward direction. The length of a path in a weighted graph is the sum of the weights along these edges e1, ..., en−1. We call this property "length" even though for some graphs it may represent some other quantity: for example, money or time.
To implement MapQuest, we need to solve the following shortest-path problem:
Given two vertices v and v', what is the shortest path through the graph that goes from v to v' ? That is, the path for which summing up the weights along all the edges from v to v' results in the smallest sum possible.
Clearly we can solve this problem by solving a more general problem, the single-source shortest-path problem:
Given a vertex v, what is the length of the shortest path from v to every vertex v' in the graph?
It is this problem that we will now investigate. Although this algorithm appears less efficient than simply finding the shortest path to the desired destination, it can be terminated early once the desired destination is found.
Here is the solution we are looking for. Each city is annotated with its minimum distance to the source vertex. The edges followed along the minimum-distance paths are shown in blue.
There are a couple of things to notice here. First, the edges on the shortest paths form a tree. This can always be arranged to be true. Second, the shortest path to a vertex v must consist of a shortest path to a predecessor vertex, followed by a single edge to v. Therefore, if we have the shortest distances to all the vertices, we can find the corresponding shortest paths by working backward. For example, Pittsburgh is at distance 7, so the shortest path must have come from edge with weight 5 from Buffalo at distance 2, because 2+5=7. The path edge into Buffalo must be the edge with weight 1 from Syracuse at distance 1, and so on.
The single-source shortest path problem can also be formulated on an undirected graph; however, it is most easily solved by converting the undirected graph into a directed graph with twice as many edges, and then running the algorithm for directed graphs. There are other shortest-path problems of interest, such as the all-pairs shortest-path problem: find the lengths of shortest paths between all possible (source–destination) pairs. This can be solved by running Dijkstra's algorithm repeatedly for each possible source, but the Floyd-Warshall algorithm is asymptotically more efficient: O(V3).
Here is a possible graph interface that we might use to write an algorithm to find shortest distances.
Turn on JavaScript to see code
There are some constraints on the running time of certain operations in this specification. Importantly, we assume that given a vertex, we can traverse the outgoing edges in constant time per edge. Not all graph implementations do not have these properties (an adjacency matrix does not), but an adjacency list representation like the following does:
Let's consider a simpler problem: solving the single-source shortest path problem for an unweighted directed graph. In this case we are trying to find the smallest number of edges that must be traversed in order to get to every vertex in the graph. This is the same problem as solving the weighted version where all the weights happen to be 1.
Do we know an algorithm for determining this? Yes: breadth-first search. The running time of that algorithm is O(V+E) where V is the number of vertices and E is the number of edges, because it pushes each reachable vertex onto the queue and considers each outgoing edge from it once. There can't be any faster algorithm for solving this problem, because in general the algorithm must at least look at the entire graph, which has size O(V+E).
We saw in recitation that we could express both breadth-first and depth-first search with the same simple algorithm that varied just in the order in which vertices are removed from the queue. We just need an efficient implementation of sets to keep track of the vertices we have visited already. A hash table fits the bill perfectly with its O(1) amortized run time for all operations. Here is an imperative graph search algorithm that takes a source vertex v0 and performs graph search outward from it:
This code implicitly divides the set of vertices into three sets:
Except for the initial vertex v0,
the vertices in set 2 are always neighbors of vertices in set 1. Thus, the
queued vertices form a frontier in the graph, separating sets 1 and 3. The expand
function moves a frontier vertex into the completed set and then expands the
frontier to include any previously unseen neighbors of the new frontier vertex.
The kind of search we get from this algorithm is determined by the dequeue
function, which selects a vertex from a queue. If q
is a FIFO
queue, we do a breadth-first search of the graph. If q
is a LIFO
queue, we do a depth-first search.
If the graph is unweighted, we can use a FIFO queue and keep track of
the number of edges taken to get to a particular node. We augment the visited
set to keep track of the number of edges traversed from v0;
it becomes a map dist
from vertices to edge counts (ints), possibly
implemented as a hash table (an even more imperative alternative is to store
distances directly in the vertices).
The only algorithmic modification needed is in expand
, which adds to the
frontier a newly found vertex at a distance one greater than that of its
neighbor already in the frontier:
Now we can generalize to the problem of computing the shortest path between
two vertices in a weighted graph. We can solve this problem by making minor
modifications to the BFS algorithm for shortest paths in unweighted graphs. As
in that algorithm, we keep a visited map that maps vertices to the best
path lengths found so far from the source vertex
v0. We
change expand
so that instead of adding 1 to the distance, its adds
the weight of the edge traversed. Furthermore, when we find an already visited
vertex, we update its distance only if the new distance is less than the old
distance.
Each time that expand
is called, a vertex is moved from the
frontier set to the completed set. Dijkstra's algorithm is an example of a greedy
algorithm, because it just chooses the closest frontier vertex at every
step. In a greedy algorithm, a locally optimal, "greedy" step turns out to
lead to a globally optimum. We can see that this algorithm finds the shortest-path
distances in the graph example above, because it will successively move B and C
into the completed set, before D, and thus D's recorded distance has been
correctly set to 3 before it is selected by the priority queue.
Let's see what happens when we run this algorithm on the example graph above,
by showing the contents of dist
after each iteration of the
while
loop. At each step, the vertices in the priority queue
are shown in red. Completed vertices are shown in gray, and unvisited vertices
are shown with a dash.
Ithaca Syracuse Albany Buffalo Pittsburgh 0 - - - - 0 1 4 - 8 (expanded Ithaca) 0 1 3 2 8 (expanded Syracuse) 0 1 3 2 7 (expanded Buffalo) 0 1 3 2 7 (expanded Albany) 0 1 3 2 7 (expanded Pittburgh)
There are two natural questions to ask at this point: How fast is it? And does it always work?
Every time the main loop executes, one vertex is extracted from the queue.
Assuming that there are V vertices in the
graph, the queue may contain O(V)
vertices. Each extract_min
operation takes O(lg V)
time assuming the heap implementation of priority queues. So the total time
required to execute the main loop itself is O(V
lg V). In addition, we must consider the time spent in the
function expand
, which applies the function handle_edge
to each outgoing edge. Because expand
is only called once per
vertex, handle_edge
is only called once per edge. It might call insert(v')
,
but there can be at most V such calls
during the entire execution, so the total cost of that case arm is at most O(V
lg V). The other case arm may be called O(E)
times, however, and each call to increase_priority
takes O(lg
V) time with the heap implementation. Therefore the total run time
is O(V lg V + E lg V),
which is O(E lg V) because V
is O(E) assuming a connected graph.
There is a more complicated priority-queue implementation called a Fibonacci heap that implements
incr_priority
in O(1) time, so that the asymptotic complexity of Dijkstra's algorithm becomes O(V lg V + E). However, this bound is largely of theoretical interest because large constant factors make Fibonacci heaps impractical for most uses. Another heap structure called a pairing heap hasincr_priority
that runs in O(lg lg n), but has better constant factors.
Clearly the algorithm terminates, because a vertex can only be inserted once and every loop removes some vertex from the frontier. Showing it computes minimum distances is more interesting.
Let's write dist(v) to mean the distance recorded in the map
dist
for the vertex v. We will consider this to
be equal to ∞ for any vertex not yet visited.
The algorithm works because it maintains a loop invariant that
holds every time the while
loop is executed:
The base case is when the loop starts. The invariant obviously holds because the only visited vertex is v0 itself, at recorded distance 0.
When the loop finishes, all vertices have been visited, and all paths are
internal paths, so (1) means that dist
contains the minimum distances to every vertex.
Now we need to show that executing the loop once preserves the loop invariant. That is, if the invariant holds for all previous loop iterations, it holds on the current one. This is an argument by induction on the number of loop iterations. Each step of the main loop promotes the closest frontier vertex v to the completed set. This clearly preserves (2), because the remaining frontier vertices have higher distances than v, and any new frontier vertices added by following edges out of v will also have higher distances.
For (1) to be maintained, it must be the case that dist(v) for the closest frontier vertex is also the shortest internal-path distance to that vertex. However, adding v to the completed set creates no new internal paths to v, so the existing distance must still be the best internal-path distance.
We need to show that the invariant (1) holds on all the other visited vertices
too. Consider what happens to the shortest internal-path distance to
some other vertex v', as depicted on the
right. We need to show that dist
records this distance to
satisfy part 1 of the invariant. There are three cases to consider:
v' is a completed node. By the invariant (2), v is already at a larger distance than v', so it can't create any shorter internal paths to v'.
This also shows that incr_priority
cannot be called on
a vertex that is not in the priority queue. It's
called only if a shorter path has been found to a
completed vertex v'. As we just argued,
this shorter path cannot exist.
v' is an unvisited node. This means there was no previous internal path to v'. So the path found through v must be the best internal path.
v' is a frontier node. There is some existing shortest internal path to v', whose distance is recorded already. If making v a completed vertex creates a shorter internal path to v, there must be an internal path that includes v and ends in v'. Consider a path like the one in gray that has some internal node v'' following v. This path can't be the shortest path because the already existing shortest path from v0 to v must be shorter than the path from v0 to v to v'', because v is farther than v'', by the invariant (2).
Therefore, the only way that a new shortest internal path
can be created to v' is if the
immediately previous node on the path is v
itself. But if we consider what the code does, we see that
it updates the distance to all such successors v' by comparing the
two possible candidates for best internal path: the best previous internal path (d'
)
and the best internal path (d+w
) that goes through v
immediately before v'.
Therefore each loop iteration preserves both parts of the invariant, and the algorithm correctly computes minimum distances to all vertices.
Part 1 of the invariant also implies that we can use Dijkstra's algorithm a little more efficiently to solve the simple shortest-path problem in which we're interested only in a particular destination vertex. Once that vertex is dequeued from the priority queue, the traversal can be halted because its recorded distance is correct. Thus, to find the distance to a vertex v the traversal only needs to visit the graph vertices that are at least as close to the source as v is.