Dijkstra's & Bellman-Ford
CS205 Data Structures
Use arrow keys or buttons to navigate
Given a weighted graph, find the path from a source vertex to a destination vertex that has the minimum total weight.
Your GPS doesn't find the route with the fewest turns -- it finds the route with the least total travel time. Each road segment has a "weight" (time to drive it). The shortest path algorithm finds the optimal route.
A->C->D costs only 3, even though A->B->D has fewer intermediate steps. Weights matter more than hop count.
Find shortest paths from one source vertex to all other vertices.
Find shortest paths between every pair of vertices.
This deck covers single-source algorithms: Dijkstra's and Bellman-Ford. These solve the most common shortest-path scenario.
Invented by Edsger Dijkstra in 1956. The core idea is a greedy strategy:
Imagine a "cloud" of certainty expanding from the source. At each step, the closest unvisited vertex joins the cloud. Once inside the cloud, its shortest distance is guaranteed correct.
If all edge weights are non-negative, there's no way a later vertex could provide a shorter path to an already-finalized vertex.
dist[] -- current best-known distance from source to each vertexparent[] -- predecessor of each vertex on the shortest path (for path reconstruction)PQ -- min-priority queue ordered by distance (tells us which vertex to process next)dist[source] = 0 and all others = infinity. We "know" the source is distance 0 from itself.
The heart of the algorithm: "Can we improve the path to v by going through u?" If yes, update.
The fundamental operation in shortest-path algorithms
Relaxing edge (u, v) means checking:
If yes: we found a shorter path to v via u. Update!
If no: the current path to v is already better. Do nothing.
Think of dist[v] as an overestimate that we gradually "relax" (tighten) downward until it reaches the true shortest distance.
Edges: A-B:4, A-C:2, A-D:1, B-D:3, C-D:5, D-E:2
| Vertex | A | B | C | D | E |
|---|---|---|---|---|---|
| dist[] | 0 | ∞ | ∞ | ∞ | ∞ |
| parent[] | - | - | - | - | - |
| visited | F | F | F | F | F |
PQ: {(A, 0)}
Extract A from PQ. Mark visited. Relax neighbors:
| Vertex | A | B | C | D | E |
|---|---|---|---|---|---|
| dist[] | 0 | 4 | 2 | 1 | ∞ |
| parent[] | - | A | A | A | - |
| visited | T | F | F | F | F |
PQ: {(D,1), (C,2), (B,4)}
Extract D (smallest in PQ). Mark visited. Relax:
| Vertex | A | B | C | D | E |
|---|---|---|---|---|---|
| dist[] | 0 | 4 | 2 | 1 | 3 |
| parent[] | - | A | A | A | D |
| visited | T | F | F | T | F |
PQ: {(C,2), (E,3), (B,4)}
Extract C. Mark visited. Relax:
| Vertex | A | B | C | D | E |
|---|---|---|---|---|---|
| dist[] | 0 | 4 | 2 | 1 | 3 |
| parent[] | - | A | A | A | D |
| visited | T | F | T | T | F |
PQ: {(E,3), (B,4)}
C's neighbors are all either visited (D) or lead to no improvement. The cloud grows with no distance changes.
Extract E. Mark visited. E's neighbor D is already visited. No updates.
| Vertex | A | B | C | D | E |
|---|---|---|---|---|---|
| dist[] | 0 | 4 | 2 | 1 | 3 |
| visited | T | F | T | T | T |
PQ: {(B,4)}
Extract B. Mark visited. B's neighbor D is visited. No updates.
| Vertex | A | B | C | D | E |
|---|---|---|---|---|---|
| dist[] | 0 | 4 | 2 | 1 | 3 |
| visited | T | T | T | T | T |
PQ: {} (empty -- done!)
A(0) -> D(1) -> C(2) -> E(3) -> B(4). The vertices were processed in increasing order of their shortest distance from the source.
Dijkstra gives us the distance to every vertex, but how do we find the actual path?
Use the parent[] array! Trace backwards from the destination to the source.
The parent pointers form a tree rooted at the source. Every path in this tree is a shortest path.
Why the greedy choice is safe
When Dijkstra's extracts vertex u from the priority queue, dist[u] equals the true shortest-path distance.
u is the first vertex extracted with an incorrect distancey is the first vertex on this path not yet visiteddist[y] ≤ dist[x] + w(x,y) = true distance to ydist[y] ≤ true dist to udist[u] ≤ dist[y]dist[u] ≤ true dist to uThis proof requires all edge weights to be non-negative. If any edge weight is negative, the "cloud" property breaks down -- a later vertex could create a shortcut through a negative edge.
Every vertex extracted from the PQ has its correct final shortest distance. This is the loop invariant that makes Dijkstra's correct.
| Operation | Count | Cost Each | Total |
|---|---|---|---|
| extractMin | V | O(log V) | O(V log V) |
| insert / decreaseKey | E | O(log V) | O(E log V) |
For connected graphs where E ≥ V, this simplifies to O(E log V).
| Operation | Count | Cost Each | Total |
|---|---|---|---|
| findMin (scan) | V | O(V) | O(V^2) |
| update dist | E | O(1) | O(E) |
Simpler to implement but slower. Better only for dense graphs where E is close to V^2.
Once a vertex is "finalized," Dijkstra never reconsiders it. A negative edge can create a shorter path after finalization.
Edges: A->B:1, A->C:4, B->D:-6, C->D:0
Source: A, Destination: D
Wait, that looks correct here. Let's tweak it:
Dijkstra finalized B with dist=1, but the path A->C->B costs -1. Once finalized, it is never reconsidered.
Handles negative edge weights! Invented independently by Richard Bellman (1958) and Lester Ford Jr. (1956).
Imagine dropping a stone in a pond. Each "ripple" extends the correct distances by one hop. After V-1 ripples, every vertex is reached.
Slower than Dijkstra's, but works with negative weights and can detect negative cycles.
| A | B | C | D | |
|---|---|---|---|---|
| dist | 0 | -1 | 3 | 4 |
| par | - | C | A | C |
| A | B | C | D | |
|---|---|---|---|---|
| dist | 0 | -1 | 3 | 4 |
| par | - | C | A | C |
Same as iteration 2. No changes.
After V-1 iterations, do one more relaxation pass over all edges.
A shortest path (without cycles) visits at most V vertices, so it has at most V-1 edges. Each iteration correctly extends paths by one edge. After V-1 iterations, all shortest paths are found -- unless a negative cycle exists.
When a negative cycle is detected, the algorithm should report that no valid shortest path exists (for vertices reachable through the cycle). The distance is effectively negative infinity.
A cycle in the graph where the total weight of all edges is negative.
If you can reach a negative cycle from the source, and your destination is reachable from the cycle, then:
Imagine currency exchange where converting USD -> EUR -> GBP -> USD gives you more money than you started with. You'd loop forever, making infinite money. That's a negative cycle in a currency exchange graph!
Negative edges are fine (Bellman-Ford handles them). Negative cycles make the problem ill-defined.
Choosing the right algorithm
| Property | Dijkstra's | Bellman-Ford |
|---|---|---|
| Strategy | Greedy | Dynamic Programming (relaxation) |
| Time (binary heap) | O((V+E) log V) | O(V * E) |
| Time (array) | O(V^2) | O(V * E) |
| Negative edge weights | NOT supported | Supported |
| Negative cycle detection | No | Yes |
| Space | O(V) + PQ | O(V) |
| Best for | Non-negative weights, speed | Negative weights, cycle detection |
Google Maps, Apple Maps, Waze all use variants of Dijkstra's with optimizations (A*, contraction hierarchies).
The RIP (Routing Information Protocol) uses a distributed version of Bellman-Ford. Each router shares its distance table with neighbors.
if dist[u] + w(u,v) < dist[v]:
dist[v] = dist[u] + w(u,v)
Can I improve the path to v by going through u?