The Story That Explains Dijkstra
Because it always settles the nearest point first — and roads can't have negative travel time — once a junction is settled, no later detour could ever beat it. That ripple-outward, always-take-the-cheapest strategy is exactly Dijkstra's algorithm, designed by Edsger Dijkstra in 1956 and still powering GPS, the internet, and games today.
Dijkstra's algorithm finds the shortest path from one source node to every other node in a weighted graph. It is greedy: at each step it picks the unvisited node with the smallest known distance, "settles" it as final, and uses it to improve its neighbours. The one rule: all edge weights must be non-negative.
For an edge u → v with weight w, ask: "is my path to u plus w shorter than the best I currently know for v?" If yes, relax it — update v's distance. Dijkstra is just relaxation, applied in the smartest possible order.
Dijkstra explores outward by distance — nearest nodes settle first, like ripples spreading.
The Five Steps
The Graph We'll Use
Six nodes (A–F), source A, with non-negative edge weights. Here is its adjacency list — each node and the neighbours it connects to, with the cost of each edge.
| Node | Connected to (weight) |
|---|---|
| A (source) | B (4), C (2) |
| B | A (4), C (1), D (5) |
| C | A (2), B (1), D (8), E (10) |
| D | B (5), C (8), E (2), F (6) |
| E | C (10), D (2), F (3) |
| F | D (6), E (3) |
Hands-On: Watch Each Line Run
The heart of the tutorial. Press Play (or Step) and watch the graph come alive: nodes light up as they're picked and settled, edges flash amber while being relaxed and turn green when they join the shortest-path tree, and the distance labels update live. The priority queue and distance table track state, while the exact Python line running lights up on the right.
import heapq
def dijkstra(graph, src):
dist = {v: float('inf') for v in graph}
dist[src] = 0
pq = [(0, src)]
visited = set()
while pq:
d, u = heapq.heappop(pq)
if u in visited:
continue
visited.add(u)
for v, w in graph[u]:
if d + w < dist[v]:
dist[v] = d + w
heapq.heappush(pq, (dist[v], v))
return dist
Amber node = currently being processed. Green node = settled (final distance). Indigo node = discovered, waiting in the queue. Amber edge = being relaxed right now; green edge = part of the shortest-path tree.
The Code, Line by Line — "This Line Does That"
Same program as the animation, fully annotated. Each line maps to a job the visualiser highlights.
import heapq
def dijkstra(graph, src):
dist = {v: float('inf') for v in graph} # every node unknown
dist[src] = 0 # source is 0 away from itself
pq = [(0, src)] # min-heap of (distance, node)
visited = set() # settled nodes
while pq: # until nothing left to explore
d, u = heapq.heappop(pq) # closest unsettled node
if u in visited: # stale duplicate?
continue # skip it
visited.add(u) # settle u - distance is final
for v, w in graph[u]: # look at each neighbour
if d + w < dist[v]: # cheaper path found?
dist[v] = d + w # relax: update distance
heapq.heappush(pq, (dist[v], v)) # queue it for later
return dist # shortest distance to every node
import heapq — Python's binary-heap module gives us an efficient min-priority queue.
dist = {v: float('inf') ...} — Step 1: every distance starts at ∞ (unknown).
dist[src] = 0 — the source is zero distance from itself.
pq = [(0, src)] — seed the queue with the source. Tuples sort by distance first.
visited = set() — tracks which nodes are already settled.
while pq — keep going as long as the queue has nodes to explore.
d, u = heappop(pq) — Step 2: pop the closest unsettled node in O(log V).
if u in visited: continue — the heap can hold stale duplicates; skip any node already settled.
visited.add(u) — Step 3: settle u. Its distance is now final.
for v, w in graph[u] — Step 4: iterate over u's neighbours and their edge weights.
if d + w < dist[v] — the relaxation test: is the path through u cheaper?
dist[v] = d + w — relax the edge: record the better distance.
heappush(pq, (dist[v], v)) — queue v with its new distance so it gets explored later.
return dist — the shortest distance from the source to every node.
Full Working — Step by Step
Here is the complete trace from source A. Each row is one node being settled, the distances it improves, and the queue afterwards. (stale) entries are old queue items that get skipped when popped.
| # | Settle | Relaxations | Distances after | Queue after |
|---|---|---|---|---|
| init | — | A = 0 | A0, B∞, C∞, D∞, E∞, F∞ | (0,A) |
| 1 | A (0) | B→4, C→2 | A0, B4, C2, D∞, E∞, F∞ | (2,C),(4,B) |
| 2 | C (2) | B 4→3, D→10, E→12 | A0, B3, C2, D10, E12, F∞ | (3,B),(4,B),(10,D),(12,E) |
| 3 | B (3) | D 10→8 | A0, B3, C2, D8, E12, F∞ | (4,B)*,(8,D),(10,D),(12,E) |
| 4 | D (8) | E 12→10, F→14 | A0, B3, C2, D8, E10, F14 | (10,D)*,(10,E),(12,E)*,(14,F) |
| 5 | E (10) | F 14→13 | A0, B3, C2, D8, E10, F13 | (12,E)*,(13,F),(14,F)* |
| 6 | F (13) | none | A0, B3, C2, D8, E10, F13 | (14,F)* |
Notice how B improved 4→3, D improved 10→8, E improved 12→10, and F improved 14→13 as better paths were discovered — that is relaxation in action.
| Destination | Shortest Distance | Path |
|---|---|---|
| A | 0 | A |
| B | 3 | A → C → B |
| C | 2 | A → C |
| D | 8 | A → C → B → D |
| E | 10 | A → C → B → D → E |
| F | 13 | A → C → B → D → E → F |
Reconstructing the Actual Path
The version above returns distances. To get the route, also record each node's predecessor, then walk it backwards from the target.
import heapq
def dijkstra_path(graph, src, target):
dist = {v: float('inf') for v in graph}
prev = {v: None for v in graph} # who we came from
dist[src] = 0
pq = [(0, src)]
while pq:
d, u = heapq.heappop(pq)
if d > dist[u]:
continue
for v, w in graph[u]:
if d + w < dist[v]:
dist[v] = d + w
prev[v] = u # remember the step
heapq.heappush(pq, (dist[v], v))
# walk predecessors backwards
path, node = [], target
while node is not None:
path.append(node)
node = prev[node]
return dist[target], path[::-1]
Why Non-Negative Weights?
Dijkstra settles a node and never revisits it, trusting that no future path can beat the one it just locked in. A negative edge shatters that trust: a cheaper route could appear after the node is settled, but Dijkstra has already moved on — producing a wrong answer. For graphs with negative weights, use Bellman-Ford instead.
| Property | Result |
|---|---|
| Settle once | Always correct |
| Greedy choice | Provably optimal |
| Use | Dijkstra ✓ |
| Property | Result |
|---|---|
| Settle once | May be wrong |
| Greedy choice | Breaks down |
| Use | Bellman-Ford |
Time & Space Complexity
| Implementation | Time | Space | Best For |
|---|---|---|---|
| Binary heap (heapq) | O((V + E) log V) | O(V + E) | Sparse graphs (most real graphs) |
| Simple array scan | O(V²) | O(V) | Dense graphs (E ≈ V²) |
| Fibonacci heap | O(E + V log V) | O(V + E) | Theoretical best; rarely used |
V = nodes, E = edges. Each node is popped once (V pops) and each edge can trigger a push (E pushes), and every heap operation costs O(log V) — giving O((V + E) log V). This is why the binary-heap version is the everyday default.
Benefits
When To Use It — and When Not To
Dijkstra vs Other Path Algorithms
| Algorithm | Solves | Weights | Time | Note |
|---|---|---|---|---|
| BFS | Single-source | Unweighted | O(V + E) | Fastest when all edges equal |
| Dijkstra | Single-source | Non-negative | O((V+E) log V) | The go-to weighted sorter |
| Bellman-Ford | Single-source | Any (no neg cycle) | O(V·E) | Handles negative edges |
| A* | Single-pair | Non-negative | Heuristic-dependent | Dijkstra + a heuristic |
| Floyd-Warshall | All-pairs | Any (no neg cycle) | O(V³) | Every pair at once |
Unweighted? BFS. Weighted, non-negative, one source? Dijkstra. Got negative edges? Bellman-Ford. Just one start–goal pair with a good distance estimate? A*. Need every pair on a small dense graph? Floyd-Warshall.
Golden Rules
heapq) for the priority queue. A linear scan
makes it O(V²) — fine for dense graphs, slow for sparse ones.
if u in visited: continue
(or if d > dist[u]: continue) so you skip outdated entries.
prev predecessor for every
relaxed node and walk it backwards from the target.