Dijkstra's Algorithm
Single-source shortest paths on graphs with non-negative edge weights, greedily settling the closest unsettled node using a min-priority queue.
Overview
Dijkstra computes the shortest distance from one source to every node in a graph whose edge weights are all ≥ 0. It repeatedly picks the unsettled node with the smallest tentative distance, declares that distance final, and relaxes its outgoing edges (dist[v] = min(dist[v], dist[u] + w)). It is Breadth-First Search (BFS) generalised to weights: the queue that pops "oldest first" is replaced by a Priority Queue that pops "smallest distance first".
Preconditions and complexity: non-negative weights, directed or undirected, single source. With a Binary Heap over an Adjacency List it runs in O((V + E) log V); with a plain array scan for the minimum it is O(V²), which is actually preferable on dense graphs where E ≈ V². It does not work with negative edges (see the counterexample below) and cannot detect negative cycles — that is Bellman-Ford's job.
Intuition
A mental model before the formal terms.
Picture the graph as a network of pipes of different lengths and pour water into the source. The water front reaches nodes in order of their true distance. Dijkstra simulates the front by jumping to the next "moment of arrival": the node with the smallest tentative arrival time is the one the water reaches next, and nothing that arrives later can ever shorten that time — because water cannot flow backwards through a pipe of negative length.
How it works
- Set
dist[s] = 0and every otherdist = ∞. Push(0, s)into a min-heap keyed by distance. - Pop the smallest
(d, u). Ifd > dist[u]this is a stale entry (a shorter path was found after it was pushed) — skip it. Otherwiseuis now settled. - For every edge
u → vwith weightw: ifdist[u] + w < dist[v], setdist[v] = dist[u] + w,parent[v] = u, and push(dist[v], v). This is called relaxing the edge. - Repeat until the heap is empty (or until the target is popped, if only one destination matters). Reconstruct a path by following
parentfrom the target. - Why the heap: each of the
≤ Erelaxations may push one entry, and each pop/push costsO(log(heap size)) = O(log E) = O(log V). Without a heap, finding the minimum unsettled node costsO(V)per step,O(V²)total.
Why it works
Claim: when u is popped with distance d, d is the true shortest distance δ(s, u). Suppose not; then a shorter path exists and it must leave the settled set at some edge x → y with y unsettled. Since all weights are ≥ 0, dist[y] ≤ δ(s, y) ≤ δ(s, u) < d, so y would have been popped before u. Contradiction. This step uses non-negativity — with negative weights δ(s, y) ≤ δ(s, u) can fail.
Concrete failure with a negative edge: nodes A, B, C with edges A→B = 2, A→C = 3, C→B = -2. Dijkstra pops A (0), then B (2) and settles it. Later it pops C (3) and finds 3 + (-2) = 1 < 2, but B is already final (or, in the lazy variant, B was already popped so the improvement is never propagated to B's successors). The true distance to B is 1; Dijkstra reports 2.
Complexity: each node is settled once, so V pops of settled nodes; each edge relaxes at most once from its settled tail, pushing at most E entries; total O((V + E) log V).
Recognition
How to tell a problem wants this.
- Weighted graph, all weights ≥ 0 (times, distances, costs, probabilities converted with
-log). - "Minimum cost/time to reach", "network delay", "cheapest route".
- Grids where each cell has an entry cost (path with minimum sum of values).
- If the weights are all equal, downgrade to BFS Shortest Path (Unweighted); if only 0 and 1, to 0-1 BFS.
Interactive visualization
Play, step, change the input. ← → and space work too.
| node | dist |
|---|---|
| A | 0 |
1dist = {v: ∞}; dist[source] = 0; pq = [(0, source)]2while pq not empty:3 (d, u) = pq.pop_min()4 if d > dist[u]: continue # stale entry5 for (v, w) in neighbors(u):6 if dist[u] + w < dist[v]:7 dist[v] = dist[u] + w; parent[v] = u8 pq.push((dist[v], v))9path = follow parent from target back to sourcePseudocode
1dist = {v: INF}; dist[s] = 0; heap = [(0, s)]2while heap not empty:3 d, u = heap.pop_min()4 if d > dist[u]: continue # stale entry5 for (v, w) in adj[u]:6 if dist[u] + w < dist[v]:7 dist[v] = dist[u] + w; parent[v] = u8 heap.push((dist[v], v))Implementations
11 · Min-priority queue2import heapq # heapq IS the min-heap: plain functions over a list3from math import inf4 5 6def dijkstra(adj: list[list[tuple[int, int]]], s: int) -> tuple[list[float], list[int]]:7 """adj[u] = [(v, w), ...] with w >= 0; nodes 0..n-1.8 Returns (dist, parent) with dist = inf for unreachable nodes."""92 · Initialize distances10 n = len(adj)11 dist: list[float] = [inf] * n12 parent = [-1] * n13 dist[s] = 014 heap: list[tuple[float, int]] = [(0, s)] # (dist, node) — tuples compare by distance first153 · Pop the closest unsettled node16 while heap:17 d, u = heapq.heappop(heap)18 if d > dist[u]: # stale entry: a shorter path was pushed later19 continue204 · Relax outgoing edges21 for v, w in adj[u]:22 nd = d + w23 if nd < dist[v]:24 dist[v] = nd25 parent[v] = u26 heapq.heappush(heap, (nd, v))275 · Result28 return dist, parentheapqis already a min-heap — no comparator needed; entries are(dist, node)tuples and tuple comparison starts with the distance.distis typedlist[float]becausemath.infis a float; real distances stay exact ints under the hood.- The stale check
d > dist[u]skips entries superseded by a later, shorter push —heapqhas no decrease-key. heappush(heap, (nd, v))andheappop(heap)are module functions operating on a plain list, not methods on a heap object.parentlets callers rebuild any shortest path by walking back to the-1sentinel.
Python ints are arbitrary precision — no overflow — but each heap operation carries interpreter overhead; PyPy or the O(V²) array variant can win on dense graphs.
heapqonly provides a min-heap; for a max-heap push negated keys.- If nodes are non-comparable objects, push
(dist, counter, node)with anitertools.count()tiebreaker so comparison never reaches the node. queue.PriorityQueuewrapsheapqwith locks for threads — never use it in algorithms.
- Pushing
(node, dist)— the heap orders by node id and the algorithm silently breaks. - Using
dist[u] + wafter popping instead of the poppedd— equivalent here, but mixing the two invites stale-value bugs. - Rebuilding the heap with
heapifyinside the loop instead of pushing duplicates — turns each relaxation into O(E).
- Heap orientation: C++
std::priority_queueis a max-heap by default and needsgreater<>(or negated keys); Pythonheapqis a min-heap; JavaScript/TypeScript have no heap at all, so a ~40-line binary MinHeap is hand-rolled. - Decrease-key: none of the four standard libraries offer it; all four versions use lazy deletion — push a new entry and skip stale ones on pop (
d > dist[u]). - Ordering entries: C++ pairs and Python tuples compare lexicographically, so
(dist, node)order matters; the JS/TS heap compares index 0 explicitly. - Overflow: C++ needs
long longand must not computeINF + w(UB); JS/TS doubles are exact only below 2^53 andInfinity + wis safelyInfinity; Python ints never overflow.
Complexity
Binary heap with lazy deletion (heap may hold up to E entries; log E = O(log V)). Array-based minimum selection: O(V²), better when E ≈ V². Fibonacci heap: O(E + V log V), rarely worth it in practice.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Single-source shortest paths with non-negative weights on a sparse graph (
E ≪ V²). - Point-to-point queries where you can stop at the target; add a heuristic to get A* Search.
- Grid path costs (each cell has a non-negative entry cost), road networks, latency graphs.
- Dense graphs: use the
O(V²)array version instead of the heap — fewer allocations and no log factor.
- Any negative edge weight — wrong answers silently. Use Bellman-Ford (or reweight with Johnson's algorithm for all-pairs).
- Unweighted graphs — BFS Shortest Path (Unweighted) is
O(V + E)and simpler. - All-pairs on a dense graph with
V ≤ ~500— Floyd-Warshall isO(V³), trivially simple, and handles negative edges. - Weights restricted to {0, 1} — 0-1 BFS with a deque is linear.
- "Shortest path with at most k edges" — the settle-once property breaks; use Bellman-Ford limited to k rounds or BFS over (node, hops) states.
Alternatives
Common mistakes
- Using it with negative edges "because there is no negative cycle" — a single negative edge already breaks it (the A/B/C example above).
- Forgetting the stale-entry check
if d > dist[u]: continue; correctness survives but the algorithm degrades towardO(E²)relaxations on dense graphs. - Marking a node visited when pushed instead of when popped — a node can be pushed with a non-final distance and would then never be improved.
- Storing tuples with the node first
(u, d)in the heap so it orders by node id instead of distance. - Overflow:
dist[u] + wwithdist[u] = INT_MAX— skip unreachable nodes or use a sentinel that cannot overflow (or 64-bit). - Solving "cheapest flights within k stops" with plain Dijkstra — the hop limit invalidates the greedy settle step.
Interview patterns
- Network delay time: run Dijkstra, answer is the max finite distance (or -1 if some node is unreachable).
- Path with minimum effort / swim in rising water: minimise the maximum edge instead of the sum — same algorithm, relax with
max(dist[u], w). - Probability paths: maximise a product by using a max-heap or by minimising
-log p. - State expansion: node = (cell, remaining fuel) or (node, used discount) when one extra small dimension matters.
- Bidirectional or target-terminated Dijkstra for a single destination.
- Choosing between BFS, DFS, Dijkstra and DPAdvanced
- Where does O(n log n) come from?Beginner
- Top K from a streamIntermediate
- When a hash map is the wrong choiceIntermediate
- Kth Largest Element in an ArrayIntermediate
- Network Delay TimeAdvanced
- Top K Frequent ElementsIntermediate