Link

Shortest Paths Reading

Table of contents

  1. Dijkstra’s algorithm
  2. A* search

Breadth-first search computes the s–t shortest paths in an unweighted graph. The result of running BFS is a shortest-paths tree (SPT) from a single start vertex to every other reachable vertex in the graph. As with minimum spanning trees, the SPT is implicitly represented in the edgeTo map.

In a mapping context, this is similar to finding the shortest paths in terms of number of roadway intersections. However, in the real world, the distance between roadway intersections is not always the same. How can we apply the idea of BFS to weighted graphs?

Similarly, we can find a shortest paths tree in a weighted digraph.

Shortest Paths Tree in a Weighted Digraph

Consider the shortest path from 0 to 5.

  • The shortest path from 0 to 5 uses the shortest path from 0 to 4 and the edge 4–5.
  • The shortest path from 0 to 4 uses the shortest path from 0 to 1 and the edge 1–4.
  • The shortest path from 0 to 1 uses the shortest path from 0 to 0 (distance 0) and the edge 0–1.

The shortest paths tree results from searching the graph by exploring the frontier. Finding the shortest path from 0 to 1 helps us find the shortest path from 0 to 4, which helps us find the shortest path to 5, 6, and 3. Using this idea, we can develop a shortest paths algorithm based on breadth-first search by using a priority queue ordered on distance from the start vertex as the fringe data structure.

Dijkstra’s algorithm

Dijkstra’s algorithm solves the single-source shortest paths problem: given a single start vertex s in a weighted graph, find the shortest paths from s to all other reachable vertices.

class DijkstraSP {
    private Map<Long, Long> edgeTo = new HashMap<>();
    private Map<Long, Double> distTo = new HashMap<>();

    // Assume StreetMapGraph is a directed graph with non-negative weights.
    DijkstraSP(StreetMapGraph g, long s) {
        ExtrinsicMinPQ<Long> fringe = new ArrayHeapMinPQ<Long>();
        fringe.add(s, 0);
        edgeTo.put(s, s);
        distTo.put(s, 0);
        for (long v : g.vertices()) {
            if (v != s) {
                fringe.add(v, Double.POSITIVE_INFINITY);
                distTo.put(v, Double.POSITIVE_INFINITY);
            }
        }

        while (!fringe.isEmpty()) {
            long v = fringe.removeSmallest();
            for (WeightedEdge<Long> e : g.neighbors(v)) {
                assert v == e.from();
                long w = e.to();
                double currDistance = distTo.get(w);
                double thisDistance = distTo.get(v) + e.weight();
                // This update is known as "relaxing" an edge e.
                if (thisDistance < currDistance) {
                    edgeTo.put(w, v);
                    distTo.put(w, thisDistance);
                    fringe.changePriority(w, thisDistance);
                }
            }
        }
    }
}
What is the overall order of growth of Dijkstra's algorithm assuming all fringe operations have a runtime in O(log V)?

The analysis is basically identical to Prim’s algorithm for finding a MST.

  • V calls to add.
  • V calls to remove.
  • E calls to changePriority.

The runtime is in O(V log V + V log V + E log V) = O(E log V) assuming there exists a SPT to every vertex.

Edge relaxation
To relax an edge v–w means to test whether the best known way from s to w is to go from s to v, then take the edge from v to w, and, if so, update our data structures.
Vertex relaxation
Relax all of the edges pointing from a given vertex.

An important correctness invariant for Dijkstra’s algorithm is that the shortest path to a vertex is established upon removing it from the fringe. Dijkstra’s algorithm can return the wrong shortest paths if the graph has negative edge weights.

Although Dijkstra’s algorithm solves the single-source shortest paths problem, in many applications, it’s not necessary to return the shortest path from the start vertex to every other vertex. For example, navigation directions in a map only require going from a start vertex to one particular goal vertex; not every other vertex in the graph. We can optimize our routing algorithm by reducing the search space. This simpler problem of finding the shortest path between a single pair of start and end vertices is known as single-pair shortest paths.

In order to inform the search direction in Dijkstra’s algorithm, A* search introduces a heuristic, an estimate of the distance from the start vertex to the goal vertex. For 2-d maps, we can use Euclidean distance (right triangle hypotenuse length).

A* Search Algorithm

A* search redefines the order that vertices are visited in the fringe as the current best distance to a vertex v plus the estimated distance from v to the goal vertex. Conceptually, the fringe now orders vertices in terms of the expected distance from the start vertex to the goal vertex through v.

Note that not every reachable vertex was visited during the search process and that the result is not necessarily a shortest paths tree since the path from 0 to 3 is suboptimal. This is fine because we only care about the path from 0 to 6.

As with Dijkstra’s algorithm, A* search returns the correct shortest path from the start vertex to the goal vertex under certain circumstances. In addition the correctness conditions for Dijkstra’s algorithm, A* search also depends on a consistent (monotone) heuristic. A heuristic function is considered consistent if its estimate is always less than or equal to the heuristic value of any neighbor plus the edge weight to reach that neighbor. Consider taking a course on artificial intelligence for more details about validating and defining heuristic functions.