Lecture 38 - Shortest Paths
Logistics
- Last topic lecture of the semester!
- HW6 corrections due Friday (there are no HW7 corrections)
- Tuesday discussion and Wednesday lecture will be review
- Final exam is 12pm-2 on Wed, 12/17
Heads-up about the final
Exam draft as I have it now (subject to change):
Part 1: Rust Fundamentals Fill-ins (23 pts, 1 point per blank) Part 2: What Does This Do? (8 pts, 2 points each, mix of old and new) Part 3: Shell and Git Commands (12 pts, 2 points each) Part 4: Stack and Heap Diagram (10 points) Part 5: Hand-Coding Problems (18 points, 6 points each)
- 5.1 - Write a basic function (old content)
- 5.2 - Write a function with closures (new content)
- 5.3 - Write two tests (new content)
Part 6: Computational Complexity Analysis (12 points)
- 3x "What's the computational complexity of this code" - 2 points each
- A table of algorithms asking time complexity and key data structure - 6 points Part 7: Algorithms Tracing (~ 25 points, 5 problems with 5 points each)
- BST, max-heap, BFS/DFS, Topological sort and MST, Dijkstra's Part 8: Data Structures and Algorithms Fill-ins (21 pts, 1 point per blank)
So total:
- Old ~ 42%, New ~ 58%
Learning objectives
By the end of today, you should be able to:
- Explain how Dijkstra's algorithm solves the single-source shortest path problem
- Trace Dijkstra's algorithm by hand
- Analyze the time complexity of Dijkstra's algorithm
- Know Dijkstra's algorithm uses a priority queue
Motivation for the shortest path problem
Everyday problem: Find the fastest/shortest route from A to B
Examples:
- GPS navigation (minimize time or distance or cost)
- Network routing (minimize latency)
- Flight planning (minimize cost or time)
- Many, many others
Why BFS doesn't work for weighted graphs
BFS finds shortest path in UNWEIGHTED graphs
Example where BFS fails:
A --100-- B
| |
1 1
| |
C ---1--- D
BFS from A: visits B first (fewer edges)
Path A → B = 100
But better path exists:
Path A → C → D → B = 1 + 1 + 1 = 3
BFS considers number of edges, not total weight!
Think-pair-share: Shortest path intuition
Graph:
A
3/ \4
B C
3| 1/|3
| / |
D E
4| |2
F G
Question: What's the shortest path from A to F?
Dijkstra's algorithm - The idea
Dijkstra's Algorithm (1959): Greedy algorithm for finding shortest paths
Key idea: Repeatedly pick the closest unvisited vertex, update distances to its neighbors
Intuition: If you know the shortest way to get somewhere, you can use that to find shortest ways to places nearby
The algorithm (informal)
Maintain:
- Distance to each vertex (initially ∞, except source = 0)
- Visited set (vertices with finalized shortest distance)
- Priority queue of (vertex, distance) pairs
Repeat:
- Pick unvisited vertex with smallest distance
- Mark it as visited (distance is now finalized)
- Update distances to neighbors: if going through this vertex is shorter, update!
Continue until all vertices visited
Visual intuition: Expanding frontier
Like water spreading from source:
https://www.youtube.com/shorts/X7EMDd82ZmI
Dijkstra systematically explores by increasing distance
Example: Dijkstra's algorithm trace
Graph:
A
2/ \5
B C
1| 1/|3
| / |
D E
Find shortest paths from A:
Initial:
Distances: A=0, B=∞, C=∞, D=∞, E=∞
Visited: {}
Priority Queue: [(A, 0)]
Step 1: Process A (distance 0)
Visit A
Update neighbors:
A → B: 0 + 2 = 2 (update B from ∞ to 2)
A → C: 0 + 5 = 5 (update C from ∞ to 5)
Distances: A=0, B=2, C=5, D=∞, E=∞
Visited: {A}
Priority Queue: [(B, 2), (C, 5)]
Step 2: Process B (distance 2)
Visit B
Update neighbors:
B → D: 2 + 1 = 3 (update D from ∞ to 3)
Distances: A=0, B=2, C=5, D=3, E=∞
Visited: {A, B}
Priority Queue: [(D, 3), (C, 5)]
Step 3: Process D (distance 3)
Visit D
Update neighbors:
D → C: 3 + 1 = 4 < 5 (update C from 5 to 4!)
Distances: A=0, B=2, C=4, D=3, E=∞
Visited: {A, B, D}
Priority Queue: [(C, 4), (C, 5-old)] # Will extract 4
Step 4: Process C (distance 4)
Visit C
Update neighbors:
C → E: 4 + 3 = 7 (update E from ∞ to 7)
Distances: A=0, B=2, C=4, D=3, E=7
Visited: {A, B, D, C}
Priority Queue: [(E, 7)]
Step 5: Process E (distance 7)
Visit E
No unvisited neighbors
Distances: A=0, B=2, C=4, D=3, E=7
Visited: {A, B, D, C, E}
Done!
Final shortest distances from A:
- A: 0
- B: 2 (path: A → B)
- C: 4 (path: A → B → D → C)
- D: 3 (path: A → B → D)
- E: 7 (path: A → B → D → C → E)
Another demo
https://www.cs.usfca.edu/~galles/visualization/Dijkstra.html
Why does it work?
Greedy choice: Always process the closest unvisited vertex
Correctness argument:
- When we visit a vertex v with distance d, d is the shortest distance to v
- Why? Any other path to v must go through an unvisited vertex u
- But u has distance ≥ d (we chose v as closest!)
- So path through u has length ≥ d
Key assumption: All edge weights are non-negative!
- Negative weights can break the algorithm (need Bellman-Ford instead)
Think about: Negative weights
What goes wrong with negative weights?
Drawing on the board
Dijkstra assumes: No benefit to detouring through other vertices
- True with non-negative weights
- False with negative weights
Implementation in Rust (for your reference)
#![allow(unused)] fn main() { use std::collections::{BinaryHeap, HashMap}; use std::cmp::{Ordering, Reverse}; fn dijkstra( graph: &HashMap<usize, Vec<(usize, i32)>>, // vertex = [(neighbor, weight)] source: usize, num_vertices: usize ) -> Vec<Option<i32>> { let mut distances = vec![None; num_vertices]; distances[source] = Some(0); let mut pq = BinaryHeap::new(); pq.push(Reverse((0, source))); // (distance, vertex) - min-heap }
#![allow(unused)] fn main() { while !pq.is_empty() { let Reverse((dist, u)) = pq.pop().unwrap(); // Skip if we found a better path already if let Some(current_dist) = distances[u] { if dist > current_dist { continue; } } // Process neighbors if graph.contains_key(&u) { let neighbors = graph.get(&u).unwrap(); for &(v, weight) in neighbors { let alt = dist + weight; // Update if shorter path found if distances[v].is_none() || alt < distances[v].unwrap() { distances[v] = Some(alt); pq.push(Reverse((alt, v))); } } } } distances } }
Dijkstra's complexity analysis
Time complexity:
- Each vertex added to priority queue once: O(V)
- Each edge causes at most one priority queue update: O(E)
- Each priority queue operation: O(log V)
- Total: O((V + E) log V) = O(E log V) (assuming connected graph)
Space complexity:
- Distance array: O(V)
- Priority queue: O(V) vertices at once in worst case
- Total: O(V)
Efficient! Much better than trying all paths (exponential!)
Complexity comparison
Finding shortest paths in a graph with V vertices, E edges:
| Algorithm | Problem | Time |
|---|---|---|
| BFS | Unweighted | O(V + E) |
| Dijkstra | Non-negative weights | O(E log V) |
| Bellman-Ford | Any weights (detects negative cycles) | O(VE) |
(You're not responsible for anything about Bellman-Ford)
Trade-off: More general algorithms are slower
Dijkstra's algorithm summary
Key steps:
- Initialize distances (source=0, others=∞)
- Use priority queue (min-heap) of (distance, vertex)
- Extract minimum, mark visited
- Update neighbors if shorter path found
- Repeat until all visited
Why it works:
- Greedy: always process closest vertex first
- Optimal substructure: shortest path consists of shortest paths
- Non-negative weights ensure no benefit to detouring