A Bit of History
In 1956, Dutch computer scientist Edsger W. Dijkstra was thinking about the shortest route between two cities in the Netherlands — partly to demonstrate the capabilities of a new computer to a general audience. By his own later account, he designed the algorithm in about twenty minutes, without pencil or paper, while having coffee with his fiancée at a café terrace in Amsterdam. He didn't publish it until 1959, in a short three-page paper that also introduced an algorithm for minimum spanning trees (closely related to Kruskal's algorithm and Prim's algorithm, covered in an upcoming deep dive). Dijkstra later reflected that the algorithm's elegance came from deliberately avoiding pencil and paper — forcing him to keep the method simple enough to hold entirely in his head.
Working Principle
Dijkstra's Algorithm: Always Expand the Cheapest Route First
Think of Dijkstra's algorithm like planning the cheapest route from one city to every other city.
Each edge has a cost — such as distance, travel time, or price. Starting from the source, Dijkstra always chooses the unprocessed vertex with the smallest known total cost from the source.
In other words: don't simply visit the closest-looking neighbor — continue from whichever vertex is cheapest to reach overall.
Notice something important in the graph above: there is a direct-looking route from S to A with cost 7.
But Dijkstra does not choose a vertex simply because it is visually nearby. Vertex B costs only 2 to reach, so B is processed first.
At every step, Dijkstra asks: “Which unprocessed vertex can I currently reach with the smallest total cost?”
1. Keep a tentative distance for every vertex
Dijkstra stores a value d[v] for every vertex. This means:
Initially, we only know how to reach the source:
d[S] = 0
d[A] = ∞
d[B] = ∞
d[C] = ∞
d[D] = ∞
The value ∞ simply means: “we have not found a route to this vertex yet.”
2. Use a min-priority queue
BFS uses a normal queue, but Dijkstra uses a min-priority queue.
Instead of processing vertices in the order they were discovered, the priority queue gives us the vertex with the smallest tentative distance.
cost = 2
cost = 7
Because 2 < 7, Dijkstra removes B first and explores routes through B.
3. Improve routes using relaxation
Whenever Dijkstra processes a vertex, it asks whether going through that vertex gives us a cheaper route to one of its neighbors.
This operation is called relaxation.
Since 5 < 9 , we improve C:
d[C] : 9 → 5
$$ d[v] \leftarrow \min\left(d[v],\, d[u] + w(u,v)\right) $$
In plain English:
How Dijkstra's algorithm proceeds
- Set the source distance to 0 and every other distance to ∞.
- Put the source into a min-priority queue.
- Remove the vertex u with the smallest tentative distance.
- Its shortest distance is now finalized.
- Examine every edge from u to a neighbor v.
- If going through u gives a cheaper route to v, update d[v].
- Continue until there are no reachable unprocessed vertices left.
Suppose u currently has the smallest tentative distance of every unprocessed vertex.
Because every edge weight is non-negative, taking additional edges cannot create a cheaper hidden route that somehow comes back and beats d[u]. Therefore, once u is selected as the cheapest remaining vertex, its distance is final.
Dijkstra's algorithm requires non-negative edge weights. A negative edge could make a route cheaper after Dijkstra has already finalized a vertex, breaking the algorithm's greedy assumption.
Every edge effectively costs 1.
Process vertices by
fewest edges from the source.
Uses a FIFO queue.
Edges may have different non-negative costs.
Process vertices by
smallest total path cost.
Uses a min-priority queue.
BFS asks “Which vertex is fewest edges away?”
Dijkstra asks “Which vertex is cheapest to reach?”
Analogy: Filling a Reservoir from the Cheapest Sources First
Think of \(d[v]\) as "the cheapest known price of water reaching town \(v\)." Every time you finalize the cheapest-so-far town, you check whether routing water through it makes any neighboring town's price cheaper than what you'd previously found. Because you always finalize the globally cheapest remaining option first, once a town is finalized, no future (necessarily more expensive) route could ever beat its price — which is exactly why the greedy choice is safe.
Worked Example
Consider a small weighted graph: \(A \to B\) (weight 4), \(A \to C\) (weight 1), \(C \to B\) (weight 1), \(B \to D\) (weight 1), \(C \to D\) (weight 5).
flowchart LR
A -->|4| B
A -->|1| C
C -->|1| B
B -->|1| D
C -->|5| D
| Step | Finalized | d[A] | d[B] | d[C] | d[D] |
|---|---|---|---|---|---|
| Init | — | 0 | ∞ | ∞ | ∞ |
| 1 | A | 0 | 4 | 1 | ∞ |
| 2 | C (dist 1) | 0 | 2 (via C) | 1 | 6 (via C) |
| 3 | B (dist 2) | 0 | 2 | 1 | 3 (via B) |
| 4 | D (dist 3) | 0 | 2 | 1 | 3 |
Notice how \(d[B]\) drops from 4 to 2 once \(C\) is finalized and its outgoing edge \(C \to B\) is relaxed — the direct edge \(A \to B\) (weight 4) was never actually the shortest route; going through \(C\) (weight \(1+1=2\)) is cheaper. This is exactly why Dijkstra's algorithm must consider all edges out of a vertex before that vertex's neighbors can be trusted as final.
Why Greedy Works Here
This is the direct-proof and induction machinery from Part 1 in action. Claim: when a vertex \(u\) is extracted from the priority queue (finalized), \(d[u]\) already equals the true shortest-path distance from the source.
Proof by induction on the order vertices are finalized. Base case: the source is finalized first with \(d[\text{source}]=0\), trivially correct. Inductive step: suppose every previously finalized vertex has a correct final distance. Let \(u\) be the next vertex extracted, with the smallest tentative distance among all unfinalized vertices. Any path to \(u\) must, at some point, leave the "finalized" set for the last time — crossing some edge \((x, y)\) where \(x\) is finalized and \(y\) is not. Since edge weights are non-negative, the length of that path is at least \(d[x] + w(x,y) \geq d[y] \geq d[u]\) (the last inequality holds because \(u\) was chosen to have the smallest tentative distance). So no unfinalized route can beat \(d[u]\) — it is already optimal.
The Proof Breaks with Negative Weights
The inequality \(d[x] + w(x,y) \geq d[y]\) is where non-negativity is used — if \(w(x,y)\) could be negative, a later, cheaper route through a currently-unfinalized vertex could beat an already-finalized vertex's distance, silently breaking correctness (with no error or warning — Dijkstra's algorithm will simply return a wrong, too-large answer). This is precisely the gap that the Bellman-Ford algorithm (an upcoming deep dive) is designed to fill.
Complexity Analysis
With a binary-heap priority queue, each of the \(V\) extract-min operations costs \(O(\log V)\), and each of the \(E\) relaxations may trigger a \(O(\log V)\) decrease-key (or, in the common simplified implementation, a fresh \(O(\log V)\) insertion, discarding stale entries lazily):
$$\text{Time: } O((V+E)\log V) \qquad \text{Space: } O(V)$$
A naive \(O(V^2)\) array-based implementation (scan all unfinalized vertices for the minimum, every time) is actually faster on very dense graphs where \(E\) approaches \(V^2\) — a good reminder that asymptotic complexity depends on the graph's shape, not just its algorithm.
Implementation
Real-World Applications
GPS Navigation and Network Routing
Every "fastest route" calculation in a mapping app is, at its core, a shortest-path problem on a weighted graph where intersections are vertices and road segments are edges weighted by estimated travel time. Production systems use highly optimized variants (contraction hierarchies, bidirectional search, A* with geographic heuristics — previewed in an upcoming deep dive), but the correctness guarantee they all build on is exactly Dijkstra's greedy relaxation argument. The same algorithm (or its variants) also computes optimal routing tables inside internet routers (OSPF, a link-state routing protocol, literally runs a Dijkstra computation on the network topology).
Exercises
- Run Dijkstra's algorithm by hand on the graph in the worked example, but starting from vertex \(C\) instead of \(A\).
- Construct a small graph with a negative edge weight where Dijkstra's algorithm produces an incorrect shortest-path distance, and explain exactly which step of the correctness proof fails.
- Modify the Python implementation above to also track and reconstruct the shortest path (not just its length) using a parent map, matching the technique from the BFS deep dive.
- Challenge: Explain why running Dijkstra's algorithm from a single source and stopping early the moment your specific target vertex is finalized is still correct and often faster in practice, even though it doesn't compute distances to every vertex.
Limitations
No Negative Weights, No Free Lunch on Density
Dijkstra's algorithm requires non-negative edge weights (see the correctness proof above) — use the Bellman-Ford algorithm when negative weights are possible. And while \(O((V+E)\log V)\) is excellent for sparse graphs, on very dense graphs the constant-factor overhead of heap operations can make a simpler \(O(V^2)\) array scan faster in practice — always profile before assuming the asymptotically better algorithm wins on your actual data.