PyInfo
Graph Algorithms contents

Dijkstra's Algorithm

Shortest paths from one source in a graph with non-negative weights, in O((n + m) log n) with a heap, plus path restoration.

Intermediate7 min readdijkstrashortest pathheapweighted graphs

Read first: Breadth-First Search, Heaps, Deques and Bisect

You are given a directed or undirected weighted graph with nn vertices and mm edges, all weights non-negative, and a source vertex ss. Dijkstra's algorithm finds the length of the shortest path from ss to every other vertex (the single-source shortest paths problem), and the paths themselves. It was described by Edsger Dijkstra in 1959.

Algorithm

Keep an array d[v]d[v] with the length of the best path found so far from ss to vv. Initially d[s]=0d[s] = 0 and d[v]=∞d[v] = \infty for the rest. Repeat:

  1. Among the vertices not yet finalized, pick vv with the smallest d[v]d[v] and finalize it.
  2. Relax all edges (v,u)(v, u) with weight ww: if d[v]+w<d[u]d[v] + w < d[u], set d[u]=d[v]+wd[u] = d[v] + w.

The idea is that the closest unfinalized vertex can't be improved anymore: any other route to it would have to leave through some unfinalized vertex which is at least as far, and weights are non-negative.

Proof sketch

Claim: when vv is finalized, d[v]d[v] is the true shortest distance. Suppose a shorter path PP to vv existed. It must leave the set of finalized vertices at some edge (q,p)(q, p) with qq finalized and pp not. By induction d[q]d[q] was exact, so after relaxing from qq, d[p]≤len(P up to p)≤len(P)<d[v]d[p] \le \text{len}(P \text{ up to } p) \le \text{len}(P) < d[v], contradicting the choice of vv as the minimum.

The proof uses non-negative weights in "len(P up to p)≤len(P)\text{len}(P \text{ up to } p) \le \text{len}(P)". With negative edges the algorithm can return wrong answers: use Bellman-Ford.

Implementation with a heap

Finding the minimum by scanning is O(n)O(n) per step, giving O(n2)O(n^2) overall. With a binary heap (heapq) the minimum comes out in O(log⁡n)O(\log n). Python's heap has no "decrease key", so we use lazy deletion: push a new (distance, vertex) entry each time we improve a distance, and skip stale entries when they are popped.

python
import heapq

INF = float("inf")

def dijkstra(adj, s):
    """adj[v] = list of (neighbour, weight). Return (dist, parent)."""
    n = len(adj)
    dist = [INF] * n
    parent = [-1] * n
    dist[s] = 0
    heap = [(0, s)]
    while heap:
        d, v = heapq.heappop(heap)
        if d > dist[v]:                       # stale entry: a shorter path was found later
            continue
        for u, w in adj[v]:
            nd = d + w
            if nd < dist[u]:
                dist[u] = nd
                parent[u] = v
                heapq.heappush(heap, (nd, u))
    return dist, parent

adj = [
    [(1, 7), (2, 9), (5, 14)],       # 0
    [(0, 7), (2, 10), (3, 15)],      # 1
    [(0, 9), (1, 10), (3, 11), (5, 2)],
    [(1, 15), (2, 11), (4, 6)],
    [(3, 6), (5, 9)],
    [(0, 14), (2, 2), (4, 9)],
]
dist, parent = dijkstra(adj, 0)
assert dist == [0, 7, 9, 20, 20, 11]

Complexity. Each edge causes at most one push, so the heap holds O(m)O(m) entries and the total is O((n+m)log⁡n)O((n + m)\log n) (or O(mlog⁡n)O(m \log n) for connected graphs). Memory O(n+m)O(n + m).

Restoring a shortest path

Like BFS, follow parent pointers back from the target:

python
def restore_path(parent, t):
    path = []
    while t != -1:
        path.append(t)
        t = parent[t]
    return path[::-1]

assert restore_path(parent, 4) == [0, 2, 5, 4]
assert restore_path(parent, 0) == [0]

Early exit

If you only need the distance to one target tt, stop as soon as tt is popped: its distance is final.

Checking it

Compare with a different algorithm (Bellman-Ford) on random graphs:

python
import random

def bellman_ford_simple(n, edges, s):
    dist = [INF] * n
    dist[s] = 0
    for _ in range(n - 1):
        for u, v, w in edges:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
    return dist

random.seed(7)
for _ in range(300):
    n = random.randint(1, 10)
    edges = [(random.randrange(n), random.randrange(n), random.randint(0, 9)) for _ in range(random.randint(0, 25))]
    g = [[] for _ in range(n)]
    for u, v, w in edges:
        g[u].append((v, w))
    s = random.randrange(n)
    assert dijkstra(g, s)[0] == bellman_ford_simple(n, edges, s)

Dense graphs: O(n2)O(n^2) without a heap

For a dense graph (m≈n2m \approx n^2), the heap does not help: pick the minimum by a linear scan and relax from an adjacency matrix. The cost is O(n2)O(n^2), which beats O(mlog⁡n)O(m \log n) when m≫n2/log⁡nm \gg n^2/\log n.

python
def dijkstra_dense(matrix, s):
    """matrix[u][v] = weight or INF if there is no edge."""
    n = len(matrix)
    dist = [INF] * n
    dist[s] = 0
    done = [False] * n
    for _ in range(n):
        v = -1
        for u in range(n):
            if not done[u] and (v == -1 or dist[u] < dist[v]):
                v = u
        if dist[v] == INF:
            break
        done[v] = True
        row = matrix[v]
        for u in range(n):
            if dist[v] + row[u] < dist[u]:
                dist[u] = dist[v] + row[u]
    return dist

n = len(adj)
mat = [[INF] * n for _ in range(n)]
for v in range(n):
    for u, w in adj[v]:
        mat[v][u] = min(mat[v][u], w)
assert dijkstra_dense(mat, 0) == dist

Pitfalls

  • Negative weights break correctness. A zero-weight edge is fine.
  • Use float("inf") (or a value larger than any possible path) as infinity. Adding to inf stays inf, so no special handling is needed in Python.
  • The stale-entry check if d > dist[v]: continue is essential for the stated complexity: without it, a vertex may be expanded many times.
  • Unreachable vertices keep dist == INF.
  • If all weights are equal use BFS; if they are only 00 or 11 use 0-1 BFS.

Practice problems

This article is a Python adaptation of “Dijkstra Algorithm” from cp-algorithms.com, licensed under CC BY-SA 4.0. The text was condensed and rewritten and the C++ code was reimplemented in Python; this adaptation is shared under the same license.