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.
12.1Single-source shortest paths
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.
The best source-to-v distance known so far. Initially d[s] = 0 and every other estimate is ∞.
The previous vertex on the current best path. Initially NIL.
Relaxing an edge
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).
for each vertex v in G.V: v.d = ∞ v.π = NIL s.d = 0
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.
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.- Initialize d[s]=0, all other distances to ∞, and every predecessor to NIL.
- Extract the unsettled vertex u with minimum d[u]; its distance is now settled.
- Relax every outgoing edge (u,v). If d[u]+w(u,v) is smaller, update both d[v] and π[v].
- 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.
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)
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 type | Representation | Priority structure | Worst-case time | Recommendation |
|---|---|---|---|---|
| Sparse E ≪ V² | Adjacency list | Binary min-heap | O((V+E) log V) ≈ O(E log V) | Recommended |
| Dense E = Θ(V²) | Adjacency list | Binary min-heap | O(V² log V) | Usually avoid |
| Dense E = Θ(V²) | Adjacency matrix | Simple array | O(V²) | Recommended |
| Dense E = Θ(V²) | Adjacency matrix | Binary min-heap | O(V² log V) | No real benefit |
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.
Some negative-edge graphs happen to be processed in a safe order.
If a cheaper path appears after a vertex is finalized, its outgoing consequences are missed.
If a reachable cycle has negative total weight, no finite shortest path exists for affected destinations.
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.
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
12.5Shortest-path variants and unweighted graphs
To find a path from every vertex to one destination, reverse every directed edge and solve one single-source problem from that destination.
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.
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.
| Property | BFS | DFS |
|---|---|---|
| Frontier structure | FIFO queue | Stack or recursion |
| Exploration order | Nondecreasing distance in edges | One branch as deeply as possible |
| Shortest-path guarantee | Yes, for unweighted/equal-weight graphs | No, in general |
| Running time | O(V+E) | O(V+E) |
12.6Minimum spanning trees
- 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.
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.
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)
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.
find identifies components; union merges two components after acceptance.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
12.9Questions and exercises
Exact directed-graph exercises from the slides
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.
Source A; directed edges A→C(1), C→B(2), C→D(4), and B→E(4). Complete the shortest-path calculation from A.
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.
- Run Dijkstra on the exercise graphs and record every distance/predecessor update.
- Solve the A–G MST exercise above using both algorithms.
- Compare DFS and BFS: backtracking, traversal order, and data structure.
- All-pairs shortest paths with no negative edges: run Dijkstra once from each vertex. Explain why this does not handle negative edges.
- Course prerequisites: construct a topological order; if a directed cycle exists, return “Not possible.”
- Reverse a directed graph by replacing every v→w with w→v. Time: O(V+E).
- 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.