Dijkstra's algorithm
With non-negative weights, the unsettled node with the smallest tentative distance is already final. A heap finds it each round.
Why breadth-first search is not enough
Breadth-first search finds the path with the fewest edges. Once edges carry weights (times, costs, distances), the fewest edges and the smallest total are different things.
The single-source shortest path problem asks for the smallest total weight from one source node to every other node. Dijkstra's algorithm solves it whenever no weight is negative.
The idea
Keep a tentative distance for every node: the best total found so far, starting at 0 for the source and
infinity elsewhere. Improving a tentative distance through an edge is called relaxing it: if
dist[u] + w < dist[v] for an edge u → v of weight w, set dist[v] = dist[u] + w.
The key claim: among the nodes not yet settled, the one with the smallest tentative distance is already final. Any other route to it would have to leave the settled region through some other unsettled node, whose tentative distance is at least as large, and then cross further edges that cannot be negative. So nothing can beat it. Settle it, relax its edges, and repeat.
That argument is the whole reason Dijkstra needs non-negative weights. A negative edge could make the "further edges" reduce the total, and the claim fails. The second lesson of this module shows it happening.
The algorithm, with a heap
Finding the smallest unsettled distance by scanning costs O(n) per round. A min-heap of (distance, node) pairs does it in O(log n). Rather than updating an entry inside the heap when a distance improves, push a new entry and ignore old ones when they come out: a popped node that is already settled is a stale entry.
Dijkstra's algorithm
dijkstra(graph, s)
- Set
dist[s] = 0and every other distance to ∞. Push(0, s). - While the heap is not empty, pop the smallest
(d, u). - If
uis already settled, skip it (stale). Otherwise mark it settled:dis final. - For each edge
u → vof weightw: ifd + w < dist[v], setdist[v] = d + wand push(d + w, v).
Step by step
From node 0. The number on each node is its tentative distance; settled nodes are final.
Dijkstra's algorithm from node 0, with a heap and lazy deletion.
-
Step 1 / 11heap (distance:node), smallest first0:00
Every distance starts at ∞ except the source 0, at 0. The heap holds (0, 0).
-
Step 2 / 11heap (distance:node), smallest first2:207:11
Pop (0, 0). Nothing left in the heap is smaller, so 0 is final for 0. Relax its edges: 1: ∞ → 7; 2: ∞ → 2.
-
Step 3 / 11heap (distance:node), smallest first5:107:1110:3212:43
Pop (2, 2). Nothing left in the heap is smaller, so 2 is final for 2. Relax its edges: 1: 7 → 5; 3: ∞ → 10; 4: ∞ → 12.
-
Step 4 / 11heap (distance:node), smallest first7:109:3110:3212:43
Pop (5, 1). Nothing left in the heap is smaller, so 5 is final for 1. Relax its edges: 3: 10 → 9.
-
Step 5 / 11heap (distance:node), smallest first9:3010:3112:42
Pop (7, 1): 1 was already settled at 5, so this entry is stale. Skip it.
-
Step 6 / 11heap (distance:node), smallest first10:3010:4112:4215:53
Pop (9, 3). Nothing left in the heap is smaller, so 9 is final for 3. Relax its edges: 4: 12 → 10; 5: ∞ → 15.
-
Step 7 / 11heap (distance:node), smallest first10:4012:4115:52
Pop (10, 3): 3 was already settled at 9, so this entry is stale. Skip it.
-
Step 8 / 11heap (distance:node), smallest first12:4013:5115:52
Pop (10, 4). Nothing left in the heap is smaller, so 10 is final for 4. Relax its edges: 5: 15 → 13.
-
Step 9 / 11heap (distance:node), smallest first13:5015:51
Pop (12, 4): 4 was already settled at 10, so this entry is stale. Skip it.
-
Step 10 / 11heap (distance:node), smallest first15:50
Pop (13, 5). Nothing left in the heap is smaller, so 13 is final for 5. No edge from it improves a distance.
-
Step 11 / 11heap (distance:node), smallest firstempty0
Pop (15, 5): 5 was already settled at 13, so this entry is stale. Skip it. The heap is empty: every reachable distance is final.
just settledin the heapsettlededge on a shortest path
The amber edges form the shortest-path tree: each node's edge from the node that gave it its final distance. Recording that predecessor whenever a distance improves is how the path itself, not just its length, is recovered.
Implementation
Directed weighted edges, distances from a source, −1 for nodes it cannot reach:
import heapq
def shortest_distances(n, edges, src):
adj = [[] for _ in range(n)]
for u, v, w in edges:
adj[u].append((v, w))
dist = [-1] * n # -1 until settled
heap = [(0, src)]
while heap:
d, u = heapq.heappop(heap)
if dist[u] != -1:
continue # stale entry: u is already final
dist[u] = d # smallest unsettled distance: final
for v, w in adj[u]:
if dist[v] == -1:
heapq.heappush(heap, (d + w, v))
return dist#include <functional>
#include <queue>
#include <utility>
#include <vector>
std::vector<long long> solve(int n, const std::vector<std::vector<int>>& edges, int src) {
std::vector<std::vector<std::pair<int, int>>> adj(n);
for (const auto& e : edges) adj[e[0]].push_back({e[1], e[2]});
std::vector<long long> dist(n, -1); // -1 until settled
using Entry = std::pair<long long, int>;
std::priority_queue<Entry, std::vector<Entry>, std::greater<Entry>> heap;
heap.push({0, src});
while (!heap.empty()) {
auto [d, u] = heap.top();
heap.pop();
if (dist[u] != -1) continue; // stale entry
dist[u] = d; // final
for (auto [v, w] : adj[u])
if (dist[v] == -1) heap.push({d + w, v});
}
return dist;
}import java.util.*;
class Solution {
public int[] shortestDistances(int n, int[][] edges, int src) {
List<List<int[]>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) adj.get(e[0]).add(new int[]{e[1], e[2]});
int[] dist = new int[n];
Arrays.fill(dist, -1); // -1 until settled
PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
heap.add(new int[]{0, src});
while (!heap.isEmpty()) {
int[] top = heap.poll();
int d = top[0], u = top[1];
if (dist[u] != -1) continue; // stale entry
dist[u] = d; // final
for (int[] e : adj.get(u))
if (dist[e[0]] == -1) heap.add(new int[]{d + e[1], e[0]});
}
return dist;
}
}function shortestDistances(n, edges, src) {
const adj = Array.from({ length: n }, () => []);
for (const [u, v, w] of edges) adj[u].push([v, w]);
// binary min-heap of [distance, node]
const heap = [];
const push = e => { heap.push(e); let i = heap.length - 1; while (i > 0) { const p = (i - 1) >> 1; if (heap[p][0] <= heap[i][0]) break; [heap[p], heap[i]] = [heap[i], heap[p]]; i = p; } };
const pop = () => { const top = heap[0], last = heap.pop(); if (heap.length) { heap[0] = last; let i = 0; for (;;) { const l = 2 * i + 1, r = l + 1; let m = i; if (l < heap.length && heap[l][0] < heap[m][0]) m = l; if (r < heap.length && heap[r][0] < heap[m][0]) m = r; if (m === i) break; [heap[m], heap[i]] = [heap[i], heap[m]]; i = m; } } return top; };
const dist = new Array(n).fill(-1); // -1 until settled
push([0, src]);
while (heap.length) {
const [d, u] = pop();
if (dist[u] !== -1) continue; // stale entry
dist[u] = d; // final
for (const [v, w] of adj[u]) if (dist[v] === -1) push([d + w, v]);
}
return dist;
}use std::cmp::Reverse;
use std::collections::BinaryHeap;
fn solve(n: i64, edges: &[[i64; 3]], src: i64) -> Vec<i64> {
let n = n as usize;
let mut adj = vec![Vec::new(); n];
for e in edges {
adj[e[0] as usize].push((e[1] as usize, e[2]));
}
let mut dist = vec![-1i64; n]; // -1 until settled
let mut heap = BinaryHeap::new();
heap.push(Reverse((0i64, src as usize)));
while let Some(Reverse((d, u))) = heap.pop() {
if dist[u] != -1 {
continue; // stale entry
}
dist[u] = d; // final
for &(v, w) in &adj[u] {
if dist[v] == -1 {
heap.push(Reverse((d + w, v)));
}
}
}
dist
}import qualified Data.Map.Strict as M
import qualified Data.Set as S
solve :: Int -> [[Int]] -> Int -> [Int]
solve n edges src = [M.findWithDefault (-1) v final | v <- [0 .. n - 1]]
where
adj = M.fromListWith (++) [(u, [(v, w)]) | [u, v, w] <- edges]
final = go (S.singleton (0, src)) M.empty
-- the set of (distance, node) is the heap; the map holds settled nodes
go heap dist = case S.minView heap of
Nothing -> dist
Just ((d, u), rest)
| M.member u dist -> go rest dist -- stale entry
| otherwise ->
let dist' = M.insert u d dist
next = [(d + w, v) | (v, w) <- M.findWithDefault [] u adj, not (M.member v dist')]
in go (foldr S.insert rest next) dist'
The same code is the reference solution to Shortest distances from one node, where it is compiled and run against the tests in all six languages.
Each edge can push one heap entry, so the heap holds at most m entries and each operation costs O(log m) = O(log n). For a dense graph (m near n²), the version that scans an array for the minimum instead of using a heap is O(n²), which is better.