Link

Shortest Paths Reading

Complete the Reading Quiz by 3:00pm before lecture.

In the previous lecture, we learned about how different graph implementations resulted in different runtimes for depth-first search and breadth-first search.

DFS
Find a path from a given vertex, s, to every reachable vertex in the graph.
BFS
Find a shortest path from a given vertex, s, to every reachable vertex in the unweighted graph.
Dijkstra’s Algorithm
Find a shortest path from a given vertex, s, to every reachable vertex in a weighted graph.

Remember that we arrived at Dijkstra’s algorithm in lecture by swapping out the Queue used in BFS for a PriorityQueue, ensuring that the lowest-weighted edges are explored first at each point in the algorithm. The following pseudocode walks through Dijkstra’s algorithm:

dijkstras(Node s, Graph g) {
    PriorityQueue unvisited;
    unvisited.addAll(g.allVertices(), );
    unvisited.changePriority(s, 0);

    Map<Node, Integer> distances;

    while (!unvisited.isEmpty()) {
        Node n = unvisited.removeMin();
        for (Node i : n.neighbors) {
            if (distances[i] < distances[n] + g.edgeWeight(n, i)) {
                continue;
            } else {
                distances[i] = distances[n] + g.edgeWeight(n, i);
                unvisited.changePriority(i, distances[i]);
            }
        }
    }
}

The visualization below shows an example of Dijkstra’s algorithm in action:

Make sure you understand how Dijkstra’s works before proceeding. Although the algorithm is useful in an incredible number of applications within computer science, it isn’t always the best choice: the next two sections each cover a limitation.

Limitation: Negative Edge Weights

We mentioned in lecture that Dijkstra’s can fail on graphs with negative edge weights. Let’s look at a small example where this is the case.

Negative Edge Graph

What path does Dijkstra's select from vertex A to vertex C?

Negative Edge Graph Dijkstra's Solution

Dijkstra’s chooses its paths greedily; when visiting A, Dijkstra’s thought the shortest path to C utilized the edge with path weight 1. However, by visually inspecting the graph that the actual shortest path between A and C is through B for a total path weight of -95. This is because when Dijkstra’s finally visited B and discovered the edge with -100, it didn’t update C’s shortest path; Dijkstra’s will not update nodes which are not in the fringe.

Also remember that Dijkstra’s (and any other shortest path algorithm) is guaranteed to fail on any graph that contains a cycle whose overall weight is negative (ie, negative weight cycle). In fact, the “shortest path” isn’t defined on a graph with a negative weight cycle. The more times you traverse the negative weight cycle, the lower your path weight becomes, and since it’s a cycle, this can be done infinitely many times, effectively achieving an infinitely small path weight.

Limitation: Searching Many Nodes

Even with all positive edges, there are some situations where Dijkstra’s isn’t the best choice. Suppose we have a graph laid out in a grid, as below, and we want to find path from s to t.

GridGraph

How will Dijkstra’s algorithm run on the graph? It will find the correct path from s to t by searching edges starting from s in all directions until it eventually finds t. Just like BFS, Dijkstra’s traverses all the nodes it can, so many nodes in the graph will be covered before finding t. We can see visually that the node immediately to the left of s will not be on s’s shortest path to t, but BFS and Dijkstra’s will still search it.

Could we improve on this behavior? Notice that in this simple grid graph, it is generally preferable for our shortest path search to travel to the right from node s when finding a path to t. It’s easy to see why visually, but that fact isn’t encoded anywhere in the graph’s edge weights. For our shortest path algorithm to take advantage of that extra information, we need a way for it to determine a “preference” among the available nodes it could visit.

We’d like to give the algorithm info about how far each node is from t, so it can tend toward picking the closest ones. Unfortunately, computing that distance is the same problem we’re trying to solve, so we’d need to run an entirely new instance of Dijkstra’s to solve it completely. Luckily, we have prior knowledge about this particular graph: that it is laid out in a grid with edge weights of 1. Using that knowledge, it’s efficient to approximate the distance.

Approximating Distances via Heuristics

We could calculate the “physical” (sometimes known as Euclidean) distance from each node to t fairly easily: take the square root of the summed-squared vertical and horizontal offsets in the grid. But an even easier approximation is simply adding up the vertical and horizontal offsets; this is known as the Manhattan distance.

What would the computed Manhattan distances look like for this graph?

GridGraph

Note how the highlighted node has a Manhattan distance of 5 because it is 2 units above and 3 units to the left of t.

Computing the Manhattan distance is an example of a heuristic: a strategy for comparing nodes in the graph, based on properties of the problem. Even though it is just an approximation, the Manhattan distance computation is a useful heuristic because it:

  1. decreases as we get “closer” to the goal t (for some definition of distance)
  2. is efficient to compute

The heuristic estimates graph-distance (ie, distance of the shortest path) using some other form of distance that we have prior knowledge of (ie, Manhattan distance). Importantly, the reason it’s even possible to talk about the Manhattan/Euclidean distance between nodes in this graph is because it’s laid out in a grid, so there is a meaningful notion of which nodes are close to each other “in space”.

At a high level, how would you make a modified version of Dijkstra's algorithm to take advantage of the prior knowledge for the grid graph?

Rather than sorting nodes in the Priority Queue based solely on their edge weights, we could also incorporate information about their distance to t. That way, given the choice between two nodes reachable by similar edge weights, we could prioritize visiting the one that we know lies closer to t.

For a more graphical explanation of the difference between Dijkstra’s algorithm (which doesn’t use this kind of heuristic) vs. a hypothetical shortest path algorithm with heuristic data, say we want to find path between two points in a geographical map:

pathOnMap

The points searched by Dijkstra’s will resemble a circle, as it explores in all directions:

DijkstraOnMap

But if we had access to prior knowledge about the relative direction of points on the map, we could “focus” our exploration:

BetterOnMap

In lecture, we will examine the latter type of algorithm: one that can take advantage of that information.


Reading Quiz