Chapter 12

Shortest Paths and Minimum Spanning Trees

Single-source shortest paths on weighted graphs, edge relaxation, Dijkstra’s and Bellman–Ford algorithms, shortest-path variants, and minimum spanning trees using Prim’s and Kruskal’s algorithms.

Interactive chapterUse Step to inspect one decision at a time, Play for the complete run, and Reset to start again.

12.1Single-source shortest paths

SSSPGiven a weighted graph and a source vertex s, find the minimum path cost from s to every reachable vertex.

BFS is correct for unweighted graphs because it processes vertices by number of edges from the source. It cannot guarantee that the front vertex of its queue has the smallest weighted distance.

Distance estimate d[v]

The best source-to-v distance known so far. Initially d[s] = 0 and every other estimate is ∞.

Predecessor π[v]

The previous vertex on the current best path. Initially NIL.

Relaxing an edge

RELAX(u, v, w): if d[v] > d[u] + w(u,v), set d[v] = d[u] + w(u,v) and π[v] = u

For the slide example, d(Diamond)=10, d(Ruby)=15, and w(Diamond,Ruby)=2. Relaxation changes d(Ruby) to min(15,10+2)=12. With array-held estimates, the update itself is O(1).

Single-source initialization
for each vertex v in G.V:
    v.d = ∞
    v.π = NIL
s.d = 0
Restaurant-route model from the slides

Start at KFC. Candidate routes connect KFC, Amber, Diamond, and Ruby. A shortest-path algorithm accumulates complete path cost—not merely the lightest next edge.

12.2Dijkstra’s algorithm

Dijkstra computes shortest paths from one source by repeatedly choosing the unsettled vertex with the smallest tentative distance. It is a greedy algorithm: once that vertex is extracted, its estimate becomes final. This conclusion is valid only when every reachable edge has a non-negative weight.

Use whenEdge weights are non-negative and shortest paths are required from one source.
Core stated[v] stores the best known distance; π[v] stores the predecessor used to reconstruct the path.
Main structureA min-priority queue returns the unsettled vertex with the smallest tentative distance.
Greedy invariant. Immediately after extract_min returns a vertex u, d[u] equals the true shortest-path distance δ(s,u). Any alternative route to u through an unsettled vertex cannot be cheaper because all remaining distances and edge weights are non-negative.
  1. Initialize d[s]=0, all other distances to ∞, and every predecessor to NIL.
  2. Extract the unsettled vertex u with minimum d[u]; its distance is now settled.
  3. Relax every outgoing edge (u,v). If d[u]+w(u,v) is smaller, update both d[v] and π[v].
  4. Continue until the queue is empty, or stop early after the desired destination is extracted.

Dijkstra step by step

Each frame shows one extraction or one edge-relaxation decision on the five-vertex graph.

Current vertexSettled vertexUnsettled vertex
def dijkstra(G, w, s):
    initialize_single_source(G, s)       # O(V)
    S = set()
    Q = min_heap(G.vertices)             # O(V)
    while Q:
        u = extract_min(Q)               # V times, O(log V) each
        S.add(u)
        for v in G.adj[u]:
            if relax(u, v, w):
                decrease_key(Q, v, v.d)  # O(log V)
O(V)initialization
O(V log V)extract-min
O(E log V)successful heap updates
O((V+E) log V)binary-heap total
Implementation detailWith adjacency lists and a binary heap, the standard bound is O((V+E) log V), commonly written O(E log V) for a connected graph. Path reconstruction follows predecessor pointers from the destination back to the source and reverses the resulting sequence; this costs O(number of vertices on the path).

Dijkstra’s algorithm — implementation guide

Choose the graph representation and priority structure according to graph density. In this comparison, a min-heap means a binary min-heap supporting an efficient decrease-key operation.

Graph typeRepresentationPriority structureWorst-case timeRecommendation
Sparse
E ≪ V²
Adjacency listBinary min-heapO((V+E) log V)
≈ O(E log V)
Recommended
Dense
E = Θ(V²)
Adjacency listBinary min-heapO(V² log V)Usually avoid
Dense
E = Θ(V²)
Adjacency matrixSimple arrayO(V²)Recommended
Dense
E = Θ(V²)
Adjacency matrixBinary min-heapO(V² log V)No real benefit
Practical selection ruleSparse graphUse an adjacency list with a binary min-heap. Only existing edges are scanned, and the heap makes minimum selection efficient.Dense graphUse an adjacency matrix with a simple array. Its O(V²) bound avoids heap overhead and matches the cost of examining the dense graph.
Why a heap does not help with a dense matrixWhen E=Θ(V²), a matrix already requires Θ(V²) entries to be examined. A binary heap does not remove that scan and may perform up to Θ(V²) decrease-key operations, producing an O(V² log V) worst-case bound. The array implementation instead performs V linear minimum scans plus the matrix scans, remaining O(V²).

12.3Negative edges and Dijkstra’s limitation

A negative edge does not automatically make a particular Dijkstra run fail, but Dijkstra has no correctness guarantee when negative weights are allowed. A later negative edge can improve a vertex already declared final.

May still work

Some negative-edge graphs happen to be processed in a safe order.

Can fail

If a cheaper path appears after a vertex is finalized, its outgoing consequences are missed.

Negative cycle

If a reachable cycle has negative total weight, no finite shortest path exists for affected destinations.

Key distinctionDijkstra cannot detect reachable negative-weight cycles. Bellman–Ford both handles negative edges and reports whether such a cycle is reachable from the source.

12.4Bellman–Ford algorithm

Bellman–Ford solves single-source shortest paths even when some edges are negative. Instead of making a greedy final decision, it repeatedly propagates better estimates across every edge. A shortest simple path contains at most |V|−1 edges, so |V|−1 complete passes are sufficient when no reachable negative-weight cycle exists.

Use whenNegative edge weights may occur, or the program must detect a negative cycle reachable from the source.
Pass invariantAfter pass i, every shortest path using at most i edges has been represented in the distance estimates.
Early terminationIf a complete pass makes no update, all estimates are stable and the remaining passes may be skipped.
Negative-cycle test. After |V|−1 passes, perform one additional scan. If a reachable edge can still be relaxed, its improvement can be repeated around a negative cycle; therefore affected vertices do not have finite shortest-path distances.

Bellman–Ford: pass-by-pass detection

The animation follows S→M(1), M→N(2), N→D(2), D→M(−6). The cycle M→N→D→M has total weight −2.

def bellman_ford(G, w, s):
    initialize_single_source(G, s)
    for _ in range(|V| - 1):
        for (u, v) in G.edges:
            relax(u, v, w)
    for (u, v) in G.edges:
        if d[v] > d[u] + w(u, v):
            return False  # reachable negative cycle
    return True
O(VE)worst-case running time
O(V)distance, predecessor, and optional reachability state
O(E)best case with early stopping after one stable pass
Exact example without a negative cycleThe slide graph has vertices s, t, x, y, z and directed edges s→t(6), s→y(7), t→x(5), t→y(8), t→z(−4), x→t(−2), y→x(−3), y→z(9), z→x(7), and z→s(2). Bellman–Ford finishes with d[s]=0, d[t]=2, d[x]=4, d[y]=7, and d[z]=−2. No estimate changes on the extra pass, so it returns TRUE: there is no reachable negative-weight cycle.

12.5Shortest-path variants and unweighted graphs

Single destination

To find a path from every vertex to one destination, reverse every directed edge and solve one single-source problem from that destination.

Single pair

For a source u and destination v, run a single-source algorithm from u. Dijkstra may stop when v is extracted; Bellman–Ford generally cannot.

All pairs

With non-negative weights, run Dijkstra from every vertex. The binary-heap bound is O(V(V+E) log V), or O(VE log V) for a connected graph.

Unweighted shortest paths: use BFS

When every edge has equal cost, minimizing total cost is exactly the same as minimizing the number of edges. BFS uses a FIFO queue to explore vertices in layers 0, 1, 2, … from the source.

BFS invariant. When a vertex is first discovered, its recorded level is the minimum possible number of edges from the source. Record its predecessor at that moment to reconstruct a shortest path in O(path length).
PropertyBFSDFS
Frontier structureFIFO queueStack or recursion
Exploration orderNondecreasing distance in edgesOne branch as deeply as possible
Shortest-path guaranteeYes, for unweighted/equal-weight graphsNo, in general
Running timeO(V+E)O(V+E)
Weighted graphsBFS is not a replacement for Dijkstra merely because all weights are positive. BFS is correct only when every edge has the same effective cost. For weights restricted to 0 and 1, use the specialized 0–1 BFS with a deque.

12.6Minimum spanning trees

Minimum spanning treeFor a connected, undirected, weighted graph, a spanning tree connects every vertex without a cycle. An MST is a spanning tree with minimum total edge weight.
  • Every spanning tree has exactly |V|−1 edges.
  • An MST may not be unique. If all edge weights are distinct, the MST is unique.
  • For a disconnected graph, Prim and Kruskal produce a minimum spanning forest, not one spanning tree.
  • Prim and Kruskal both rely on the cut property: a lightest edge crossing a cut that respects the partial solution is safe to add.
Cut property. Partition the vertices into two sets without cutting through an already chosen MST component. A minimum-weight edge crossing that cut belongs to at least one MST, so adding it preserves the possibility of completing an optimal tree.
Shortest paths versus MSTA shortest-path tree minimizes each route from one designated source. An MST minimizes the sum of all selected tree edges. Neither objective implies the other, even though both algorithms may look greedy.

12.7Prim’s algorithm

Prim grows a single tree from an arbitrary root. For each vertex outside the tree, key[v] is the lightest edge currently known from the tree to that vertex, and π[v] is the tree vertex that provides that edge.

Greedy choiceExtract the outside vertex with minimum key and add its parent edge to the tree.
InvariantThe chosen edges are contained in some MST; every extraction adds a safe light edge across the current cut.
Best fitA binary heap with adjacency lists is effective for sparse graphs; an O(V²) array implementation is often simpler for dense graphs.

Prim: grow the MST from vertex a

Watch the frontier keys change after each extracted vertex joins the tree.

def mst_prim(G, w, r):
    for u in G.V: u.key, u.parent = ∞, NIL
    r.key = 0
    Q = min_heap(G.V)
    while Q:
        u = extract_min(Q)
        for v in G.adj[u]:
            if v in Q and w(u,v) < v.key:
                v.parent = u
                v.key = w(u,v)
                decrease_key(Q, v, v.key)
O(V²)array implementation; suitable for dense graphs
O((V+E) log V)binary heap with adjacency lists
O(E + V log V)Fibonacci heap, amortized
Correctness and outputThe parent edges {(π[v],v) : v ≠ r} form the MST. If the queue’s minimum key is ∞ before all vertices are reached, the input is disconnected; report that no spanning tree exists, or continue from another root to build a minimum spanning forest.

12.8Kruskal’s algorithm

Kruskal begins with one component per vertex, sorts all edges by nondecreasing weight, and accepts an edge exactly when its endpoints are in different components. The accepted edge merges those components; an edge whose endpoints already share a component is rejected because it would create a cycle.

Greedy choiceProcess the globally lightest remaining edge.
Cycle testDisjoint-set find identifies components; union merges two components after acceptance.
InvariantThe accepted forest is acyclic and can be extended to an MST.

Kruskal: edge-by-edge decisions

Green edges are accepted; dashed orange edges are rejected because their endpoints are already connected.

def kruskal(G):
    A = []
    make_set(v) for each v in G.V
    for (u, v, w) in sorted(G.edges, key=weight):
        if find_set(u) != find_set(v):
            A.append((u, v, w))
            union(u, v)
    return A
O(E log E)dominant edge-sorting cost
O(E α(V))all disjoint-set operations with optimizations
37total weight of this example’s MST
Why the complexity is also written O(E log V)For a simple graph, E ≤ V², so log E = O(log V). Union by rank/size with path compression makes each disjoint-set operation nearly constant amortized time; sorting therefore dominates.

12.9Questions and exercises

Exact directed-graph exercises from the slides

Exercise graph A

Source A; edges A→B(4), A→C(1), C→B(2), C→D(4), B→E(4), and D→E(4). Run Dijkstra and record every estimate and predecessor.

Exercise graph B

Source A; directed edges A→C(1), C→B(2), C→D(4), and B→E(4). Complete the shortest-path calculation from A.

Exercise graph S

Edges S→1(1), S→4(6), S→7(7), 1→4(4), 1→2(1), 7→4(3), 7→3(2), 2→4(2), 2→3(1), 4→3(2). Final distances: 1, 4, 7, 2, 3 for vertices 1, 4, 7, 2, 3.

Exact MST exercise graphUndirected vertices A–G with edges A–D(5), A–C(7), D–C(9), C–B(8), C–E(7), B–E(5), D–E(15), D–F(6), F–E(8), F–G(11), and E–G(9). Find its MST using both Prim and Kruskal; for Prim, show every intermediate key and parent.
  1. Run Dijkstra on the exercise graphs and record every distance/predecessor update.
  2. Solve the A–G MST exercise above using both algorithms.
  3. Compare DFS and BFS: backtracking, traversal order, and data structure.
  4. All-pairs shortest paths with no negative edges: run Dijkstra once from each vertex. Explain why this does not handle negative edges.
  5. Course prerequisites: construct a topological order; if a directed cycle exists, return “Not possible.”
  6. Reverse a directed graph by replacing every v→w with w→v. Time: O(V+E).
  7. Worst-case DFS memory usage: O(V).

Dijkstra versus Prim

Both may use a min-priority queue and grow outward from a vertex. Dijkstra’s key is the total distance from the source; Prim’s key is the weight of one cheapest edge joining a vertex to the current tree. Prim may start at any vertex.