Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Coding · Graphs

Dijkstra's algorithm

Lesson · 13 min read · Graphs · Shortest paths from one source

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.

9222sabt
Breadth-first search takes the direct edge, cost 9. The three-edge route costs 6.

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)

  1. Set dist[s] = 0 and every other distance to ∞. Push (0, s).
  2. While the heap is not empty, pop the smallest (d, u).
  3. If u is already settled, skip it (stale). Otherwise mark it settled: d is final.
  4. For each edge u → v of weight w: if d + w < dist[v], set dist[v] = d + w and 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.

  1. Step 1 / 11
    7234811036001∞2∞3∞4∞5∞
    heap (distance:node), smallest first
    0:00

    Every distance starts at ∞ except the source 0, at 0. The heap holds (0, 0).

  2. Step 2 / 11
    72348110360017223∞4∞5∞
    heap (distance:node), smallest first
    2: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.

  3. Step 3 / 11
    72348110360015223104125∞
    heap (distance:node), smallest first
    5: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.

  4. Step 4 / 11
    7234811036001522394125∞
    heap (distance:node), smallest first
    7: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.

  5. Step 5 / 11
    7234811036001522394125∞
    heap (distance:node), smallest first
    9:3010:3112:42

    Pop (7, 1): 1 was already settled at 5, so this entry is stale. Skip it.

  6. Step 6 / 11
    723481103600152239410515
    heap (distance:node), smallest first
    10: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.

  7. Step 7 / 11
    723481103600152239410515
    heap (distance:node), smallest first
    10:4012:4115:52

    Pop (10, 3): 3 was already settled at 9, so this entry is stale. Skip it.

  8. Step 8 / 11
    723481103600152239410513
    heap (distance:node), smallest first
    12: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.

  9. Step 9 / 11
    723481103600152239410513
    heap (distance:node), smallest first
    13:5015:51

    Pop (12, 4): 4 was already settled at 10, so this entry is stale. Skip it.

  10. Step 10 / 11
    723481103600152239410513
    heap (distance:node), smallest first
    15:50

    Pop (13, 5). Nothing left in the heap is smaller, so 13 is final for 5. No edge from it improves a distance.

  11. Step 11 / 11
    723481103600152239410513
    heap (distance:node), smallest first
    empty0

    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

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.

TimeO((n + m) log n)
SpaceO(n + m)

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.