Dijkstra with a Priority Queue
Phase 1 gave the loop in words: repeatedly take the closest unfinalized node and use its edges to improve its
neighbors. The one piece that needs care is "take the closest node." Scanning every node each round works but
is slow. A min-priority-queue - a min-heap - hands you the smallest-distance node in O(log n), and that
is what makes Dijkstra fast enough to route traffic in real time.
The data structures
dist- a dictionary mapping each node to its best-known distance from the start. Everything begins at infinity except the start at 0.- A min-heap
pqof(distance, node)pairs. Python'sheapqkeeps the smallest pair on top, and because the distance is first, "smallest pair" means "closest node."
The algorithm, step by step
=
=
= 0
= # (distance, node), min-heap by distance
, = # closest unfinalized node
continue # stale entry - we already found a shorter route
= +
# relax: found a cheaper way to neighbor
=
return
{'A': 0, 'B': 1, 'C': 2, 'D': 3}
What just happened: trace the heap. It starts with (0, A). Popping A relaxes its edges, pushing
(1, B) and (10, C). Next the heap yields (1, B) - the closest node - and B's edge to C offers
1 + 1 = 2, beating 10, so C drops to 2 and (2, C) is pushed; B also sets D to 6. Then (2, C) pops
and improves D to 2 + 1 = 3. When the old (6, D) and (10, C) finally surface, the guard
d > dist[node] throws them away as stale. Final distances: B is 1, C is 2, D is 3.
📝 Terminology. Relaxing an edge (u, v) means: if dist[u] + weight(u, v) is less than the current
dist[v], lower dist[v] to that new, cheaper value. The whole algorithm is just relaxing edges in the
right order - closest node first.
💡 Key point: the stale-entry skip. A plain binary heap has no "decrease-key," so instead of updating an
existing entry we just push a new, smaller (distance, node). That leaves outdated pairs in the heap. The
if d > dist[node]: continue line quietly discards them: if the distance we popped is worse than the best we
have already recorded, this entry is old news. This lazy approach is simpler than a decrease-key heap and
runs in O((V + E) log V).
Getting the path, not just the distance
Distances are often not enough - you want the actual route. Track each node's predecessor as you relax, then walk the predecessors backward from the target.
=
=
= 0
=
=
, =
continue
= +
=
=
# rebuild the route by following predecessors backward
, = ,
=
return ,
(['A', 'B', 'C', 'D'], 3)
What just happened: every time a node's distance improved, we recorded which node we came from in prev.
Starting at D and following prev gives D -> C -> B -> A; reversing it yields the route A -> B -> C -> D
with total weight 3 - the same cheap detour the intuition predicted.
The same algorithm, in other languages
The moving parts never change: a dist map, a min-heap of (distance, node), pop the closest, skip stale
entries, relax edges. What changes is how each language spells "min-heap" - Python's heapq, Java's
PriorityQueue, C++'s priority_queue with greater, Go's container/heap, Rust's BinaryHeap with
Reverse. Flip through the tabs.
[[codegroup Dijkstra]]
=
= 0
=
, =
continue
= +
=
return
;
;
record
static Map
std::unordered_map<std::string, int>
package main
import (
"container/heap"
"math"
)
type Edge struct
type State struct
type PriorityQueue []State
func () int
func (i, j int) bool
func (i, j int)
func (x any)
func () any
func dijkstra(graph map[string][]Edge, start string) map[string]int
use ;
use Reverse;
[[/codegroup]]
Two details worth noticing across the tabs. C++ and Rust make a min-heap out of a max-heap: std::greater
flips the comparison, and Reverse flips the ordering, because both priority_queue and BinaryHeap are
max-heaps by default. And every version keeps the distance first in the pair, so "smallest in the heap"
always means "closest node."
Check yourself
[
{
"q": "Why does Dijkstra use a min-priority-queue?",
"choices": ["To sort the final list of distances", "To efficiently pull out the unfinalized node with the smallest tentative distance each round", "To store the graph's edges", "To detect cycles in the graph"],
"answer": 1,
"explain": "The core operation is 'expand the closest node.' A min-heap returns that node in O(log n), which is what keeps the whole algorithm fast."
},
{
"q": "What does 'relaxing' an edge (u, v) mean?",
"choices": ["Removing the edge from the graph", "If dist[u] + weight(u, v) is smaller than dist[v], lowering dist[v] to that value", "Doubling the edge's weight", "Marking v as finalized"],
"answer": 1,
"explain": "Relaxation is the update step: whenever going through u gives a cheaper route to v, dist[v] is lowered. Dijkstra is just edge relaxation in closest-first order."
},
{
"q": "In the heap-based version, why skip a popped (d, node) when d > dist[node]?",
"choices": ["It signals a bug in the graph", "It is a stale, outdated entry - a shorter route to that node was already recorded, so this one is obsolete", "Only to save a little memory", "That situation can never actually happen"],
"answer": 1,
"explain": "Without decrease-key, improved distances are pushed as new heap entries, leaving older larger ones behind. If the popped distance is worse than the recorded best, the entry is stale and is discarded."
}
]
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. Why does Dijkstra use a min-priority-queue?
2. What does 'relaxing' an edge (u, v) mean?
3. In the heap-based version, why skip a popped (d, node) when d > dist[node]?