Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Lecture 11: Shortest Path

Open In Colab

This notebook is the first of the three problem notebooks of this lecture, introduced in Algorithms and Their Characteristics. We define the shortest path problem, solve an example with an LO model in pulp, and then solve it with Dijkstra’s algorithm, the dedicated algorithm for this problem.

Learning outcomes

On completion of this notebook, you will be able to:

  • formulate the shortest path problem as an LO model and solve it with pulp;

  • explain what dW(x)d_W(x) means at every step of Dijkstra’s algorithm;

  • apply Dijkstra’s algorithm by hand in a table and find a shortest path by backtracking;

  • explain why Dijkstra’s algorithm needs non-negative distances and why its running time grows like n2n^2.

The Shortest Path Problem

Every time you ask navigation software for a route, it solves a shortest path problem: find the shortest (or fastest) route from where you are to where you want to go. The same problem appears in many less visible places, for example in routing data packets through the internet, and as a building block of other problems: the traveling salesman problem needs the shortest distance between every pair of cities.

In the notation of Graphs: Notation for This Lecture, the problem is: given a directed graph with distances cij≥0c_{ij} \ge 0 on the arcs, find a shortest path from a source node ss to a destination node dd. An undirected edge, which can be used in both directions, counts as two arcs.

As an example, we look for a shortest path from s=As = \text{A} to d=Fd = \text{F} in the graph of Figure 1. For example, the distance from D to F is cDF=5c_{DF} = 5, and the edge between B and C can be used in both directions: cBC=cCB=2c_{BC} = c_{CB} = 2.

Source
<Figure size 900x480 with 1 Axes>

Figure 1:Example network with distances along the arcs. The edge between B and C can be used in both directions.

LO Approach

Model

The shortest path problem is a special case of the transshipment problem: ship one unit from the source ss (supply 1) to the destination dd (demand 1) as cheaply as possible, where the cost of using an arc is its distance. The unit follows a path, and the cost is the length of that path.

Decision Variables

xij=1x_{ij} = 1 if the arc i→ji \to j is on the path, and 0 otherwise.

Objective

Minimize the length of the path, ∑i,jcijxij\sum_{i,j} c_{ij} x_{ij}, with cij=∞c_{ij} = \infty if there is no arc i→ji \to j.

Constraints

One unit more leaves the source than enters it, one unit more enters the destination than leaves it, and every other node passes on what it receives (flow conservation). The constraint xij≥0x_{ij} \ge 0 is needed: without it, a negative amount could flow backwards over an arc.

We do not require xij∈{0,1}x_{ij} \in \{0, 1\}. As for the transportation problem, the corner points of the feasible region are already integer, so the simplex method returns a 0/1 solution. If a solver does return a fractional solution, the unit is split over several paths, and each of these paths is then a shortest path.

Complete LO Model

min⁡∑i,jcijxijs.t.∑jxsj−∑jxjs=1∑ixid−∑jxdj=1∑ixik=∑jxkj,for all nodes k≠s,dxij≥0,for all i,j.\begin{aligned} \min \quad & \sum_{i,j} c_{ij} x_{ij} \\ \text{s.t.} \quad & \sum_{j} x_{sj} - \sum_{j} x_{js} = 1 \\ & \sum_{i} x_{id} - \sum_{j} x_{dj} = 1 \\ & \sum_{i} x_{ik} = \sum_{j} x_{kj}, \quad \text{for all nodes } k \ne s, d \\ & x_{ij} \ge 0, \quad \text{for all } i, j. \end{aligned}

One of these constraints is redundant. Every arc leaves one node and enters another, so the net outflows (outflow minus inflow) of all nodes add up to 0. If the source has net outflow 1 and every other node except the destination has net outflow 0, the destination must have net outflow -1: it receives one unit. The destination constraint therefore follows from the others. We keep it because it makes the model easier to read, and it does not change the optimal solution.

Model for the Example

For Figure 1, with source A and destination F, there is a variable for each of the 10 arcs; pairs of nodes without an arc have cij=∞c_{ij} = \infty, so their variables are 0 in any optimal solution and can be left out. No arc enters A or leaves F, which simplifies their constraints:

min⁡2xAB+xAC+2xBC+2xCB+3xBD+3xCD+xCE+xED+5xDF+2xEFs.t.xAB+xAC=1(source A)xDF+xEF=1(destination F)xAB+xCB=xBC+xBD(node B)xAC+xBC=xCB+xCD+xCE(node C)xBD+xCD+xED=xDF(node D)xCE=xED+xEF(node E)xij≥0,for all arcs i→j.\begin{aligned} \min \quad & 2x_{AB} + x_{AC} + 2x_{BC} + 2x_{CB} + 3x_{BD} + 3x_{CD} + x_{CE} + x_{ED} + 5x_{DF} + 2x_{EF} \\ \text{s.t.} \quad & x_{AB} + x_{AC} = 1 & \text{(source A)} \\ & x_{DF} + x_{EF} = 1 & \text{(destination F)} \\ & x_{AB} + x_{CB} = x_{BC} + x_{BD} & \text{(node B)} \\ & x_{AC} + x_{BC} = x_{CB} + x_{CD} + x_{CE} & \text{(node C)} \\ & x_{BD} + x_{CD} + x_{ED} = x_{DF} & \text{(node D)} \\ & x_{CE} = x_{ED} + x_{EF} & \text{(node E)} \\ & x_{ij} \ge 0, \quad \text{for all arcs } i \to j. \end{aligned}

Solving the Example in pulp

The model is built as the transshipment model in Transportation and Transshipment: the keys of the dictionary distance are the arcs, and a variable is only created for an arc that exists. The constraint of a node depends on whether it is the source, the destination, or another node.

status: Optimal
arcs used: [('A', 'C'), ('C', 'E'), ('E', 'F')]
length: 4.0

The shortest path is A → C → E → F, with length 1+1+2=41 + 1 + 2 = 4. The model has a variable for every arc and a constraint for every node. For a network with nn nodes where (almost) every pair of nodes is connected, that is about n2n^2 variables. Dijkstra’s algorithm finds the answer much faster.

Dijkstra’s Algorithm

Edsger Dijkstra (1930-2002) was a Dutch computer scientist. He designed his algorithm while working at the Mathematisch Centrum in Amsterdam, now the CWI (Centrum Wiskunde & Informatica), the Dutch national research institute for mathematics and computer science at Amsterdam Science Park. He published it in a three-page paper (Dijkstra, 1959). The algorithm finds the shortest paths from the source to all nodes at once, and it requires that all distances are non-negative, cij≥0c_{ij} \ge 0.

The idea is to grow a set WW of visited nodes, one node at a time. In every step we visit the nearest unvisited node, nearest in the sense of the shortest path that only passes through visited nodes. Dijkstra proved that once a node is visited, we know a shortest path from the source to that node.

What the Algorithm Keeps Track Of

To make every intermediate step precise, we define for a set of visited nodes WW and every node xx:

dW(x)=length of a shortest path from the source to x that only uses intermediate nodes from W.d_W(x) = \text{length of a shortest path from the source to } x \text{ that only uses intermediate nodes from } W.

“Intermediate” means the nodes between the source and xx; the source and xx themselves need not be in WW. If there is no such path, dW(x)=∞d_W(x) = \infty. For example, in Figure 1 with W={A}W = \{\text{A}\} only the direct arcs from A count: dW(B)=2d_W(\text{B}) = 2, dW(C)=1d_W(\text{C}) = 1 and dW(x)=∞d_W(x) = \infty for the other nodes. With W={A,C}W = \{\text{A}, \text{C}\}, the path A → C → E is allowed too, so dW(E)=2d_W(\text{E}) = 2.

As WW grows, more paths are allowed and dW(x)d_W(x) can only decrease. When WW contains all nodes, every path is allowed, so our aim is dall nodes(destination)d_{\text{all nodes}}(\text{destination}).

The Algorithm

Write W∖x′W \setminus x' for the set WW without the node x′x'.

  • Start: W=∅W = \emptyset, dW(source)=0d_W(\text{source}) = 0 and dW(x)=∞d_W(x) = \infty for all other nodes xx.

  • While WW does not contain all nodes:

    1. Find the node x∉Wx \notin W with the smallest dW(x)d_W(x); call it x′x'.

    2. Visit x′x': add it to WW. This makes x′x' the current node.

    3. Update, for all x∉Wx \notin W:

      dW(x)=min⁡{dW∖x′(x′)+cx′x, dW∖x′(x)}.d_W(x) = \min\{d_{W \setminus x'}(x') + c_{x'x},\ d_{W \setminus x'}(x)\}.

      When the first term is the smaller one, save x′x' as the previous node of xx.

The update considers the two possibilities for a shortest path to xx now that x′x' may be used as an intermediate node: the path goes via x′x' (first reach x′x', then take the arc x′→xx' \to x), or it does not use x′x' and the old value stays. Only arcs out of x′x' can give an improvement, so in practice we only check the unvisited neighbors of the current node, with cx′x=∞c_{x'x} = \infty for the others.

The previous nodes give the shortest paths themselves: follow them backwards from the destination to the source.

As written, the algorithm finds the shortest paths from the source to all nodes. If you only need a shortest path to the destination, you can stop as soon as the destination is visited: its value is then final (see Why It Works).

Example

We apply the algorithm to Figure 1. Table 1 shows dW(x)d_W(x) for every node xx throughout the algorithm, with the previous node in brackets. Each row shows the values after updating, for the nodes that are not yet visited, with WW the set of visited nodes in that row. The smallest of these values is underlined: its node becomes the current node x′x' in the next step.

Table 1:Distances dW(x)d_W(x) throughout Dijkstra’s algorithm, from A, for the network in Figure 1.

stepdW(A)d_W(\text{A})dW(B)d_W(\text{B})dW(C)d_W(\text{C})dW(D)d_W(\text{D})dW(E)d_W(\text{E})dW(F)d_W(\text{F})WW
start0∞∞∞∞∞{}\{\}
12 (A)1 (A)∞∞∞{A}\{A\}
22 (A)4 (C)2 (C)∞{A,C}\{A, C\}
34 (C)2 ©∞{A,C,B}\{A, C, B\}
43 (E)4 (E){A,C,B,E}\{A, C, B, E\}
54 (E){A,C,B,E,D}\{A, C, B, E, D\}
6{A,C,B,E,D,F}\{A, C, B, E, D, F\}

Step by step:

  • Start. Only the source has a finite value, so A becomes current.

  • Step 1. Visit A, so W={A}W = \{\text{A}\}. Update the neighbors of A: dW(B)=min⁡{0+2,∞}=2d_W(\text{B}) = \min\{0 + 2, \infty\} = 2 and dW(C)=min⁡{0+1,∞}=1d_W(\text{C}) = \min\{0 + 1, \infty\} = 1, both with previous node A. The smallest value among the unvisited nodes is 1, so C becomes current.

  • Step 2. Visit C. Via C, B could be reached in 1+2=31 + 2 = 3, which is worse than 2, so B keeps its value. D gets 1+3=41 + 3 = 4 and E gets 1+1=21 + 1 = 2, both via C. B and E are tied at 2; we pick B (any choice works, see Exercise 1).

  • Step 3. Visit B. Via B, D could be reached in 2+3=5>42 + 3 = 5 > 4, so nothing changes, and E becomes current.

  • Step 4. Visit E. Via E, D is reached in 2+1=3<42 + 1 = 3 < 4, so dW(D)=3d_W(\text{D}) = 3 with previous node E. F gets 2+2=42 + 2 = 4.

  • Step 5. Visit D. Via D, F could be reached in 3+5=8>43 + 5 = 8 > 4, so F keeps 4 and becomes current.

  • Step 6. Visit F. All nodes are in WW, so we stop.

Once a node is chosen as current node, its value does not change anymore, and it is the length of a shortest path from A to that node (see Why It Works). So the shortest distance of a node is the underlined value in its column. For example, D is chosen in the row of step 4, where W={A,C,B,E}W = \{\text{A}, \text{C}, \text{B}, \text{E}\} and dW(D)=3d_W(\text{D}) = 3, so the shortest path from A to D has length 3. In the same way, the underlined dW(F)=4d_W(\text{F}) = 4 in the row of step 5 is the length of a shortest path from A to F. Backtracking through the previous nodes gives the paths: the previous node of F is E, that of E is C, and that of C is A, so the shortest path from A to F is A → C → E → F, the same as pulp found. Together, the previous nodes form a tree of shortest paths from A to all nodes, shown in Figure 2.

<Figure size 900x480 with 1 Axes>

Figure 2:Result of Dijkstra’s algorithm from A: next to each node its shortest distance from A, and in red the arcs from each node’s previous node, which form the tree of shortest paths.

Why It Works

Suppose x′x' has the smallest value dW(x′)d_W(x') among the unvisited nodes. Any path from the source to x′x' that is not counted in dW(x′)d_W(x') leaves the visited nodes at some point: it reaches a first node yy outside WW, and then continues to x′x'. The part up to yy is at least dW(y)≥dW(x′)d_W(y) \ge d_W(x') long, and because all distances are non-negative, the rest of the path cannot make it shorter. So no path to x′x' is shorter than dW(x′)d_W(x'), and visiting x′x' fixes its shortest distance for good.

This argument fails with negative distances: a path could first go to a far-away node and then come back over an arc with a large negative distance. That is why Dijkstra’s algorithm requires cij≥0c_{ij} \ge 0. The argument also shows why we may stop as soon as the destination is visited: we then already know its shortest distance.

Dijkstra’s Algorithm in Python

The function below follows the algorithm line by line. You do not have to be able to write it yourself, but read it next to The Algorithm and check that you can follow each step. The dictionary d holds dW(x)d_W(x) and the list W the visited nodes. To show the steps, the function also builds the table of Table 1.

Loading...

Backtracking through previous gives the shortest path:

shortest path: A -> C -> E -> F with length 4
<Figure size 900x480 with 1 Axes>

Figure 3:Directed graph of Exercise 3, with distances along the arcs.

Running Time

How much work does Dijkstra’s algorithm take for a network with nn nodes? Every repetition of the while loop visits one new node, so there are nn repetitions. In each repetition, finding the unvisited node with the smallest value and updating the unvisited nodes each look at no more than nn nodes. In total that is about n⋅n=n2n \cdot n = n^2 operations. For a network with 1000 nodes, this is in the order of a million operations, a fraction of a second on a computer. In Complexity and Heuristics we compare this with problems for which no such fast algorithm is known.

References

  • Koole, G. (2019). An Introduction to Business Analytics. Chapter 7, “Combinatorial Optimization” (shortest path).

  • Dijkstra, E. W. (1959). A note on two problems in connexion with graphs. Numerische Mathematik, 1, 269-271.