~/graphs/dijkstra

Dijkstra's algorithm

Shortest paths from one source when edge weights are never negative. Always finish the closest unfinished node next.

what

Keep a min-heap of (distance, node). Pop the closest node, its distance is now final, and try to shorten the paths to its neighbours.

use when

You need the cheapest route from one start node, and no edge weight is negative.

time

O((V + E) log V)

space

O(V + E)

You’ll recognise it when

  • You want the cheapest or fastest route, and edges have different costs: travel times, prices, delays.
  • There is one starting point, and you need the distance to one node or to all of them.
  • Every weight is zero or more.
  • The “cost” of a path only grows as the path gets longer, such as a sum of weights or the largest step so far.

If every edge costs the same, plain BFS is simpler and faster. If some edges are negative, you need Bellman-Ford instead.

The idea

Pour water into a network of pipes at the source. The water reaches the nearest junction first, then the next nearest, and so on. When the water arrives at a junction, nothing can beat it there later: every other route is still on its way and already longer.

Dijkstra does the same with a min-heap. It keeps a best-known distance dist for every node and always takes the node with the smallest one. That node is settled: its distance is final. Then it checks each edge out of it, and if going through it is a shortcut to a neighbour, it lowers that neighbour’s dist.

How it works

The graphic runs the algorithm on a small directed graph from node 0. Scroll through the steps and the graphic follows along. You can also press play, or edit the edges and try your own graph.

  1. Set every dist to ∞, except dist[source] = 0. Put (0, source) in the heap.
  2. Pop the entry with the smallest distance, (d, u).
  3. If d still equals dist[u], then u is settled. Go through each edge from u to a neighbour v with weight w.
  4. Relax the edge: if d + w < dist[v], the route through u is shorter. Set dist[v] = d + w and push (dist[v], v).
  5. A later path can beat one found earlier. Node 1 first got 4 from the direct edge, but 0 → 2 → 1 costs only 3. dist[1] drops to 3, and the old (4, 1) entry stays in the heap.
  6. If d + w isn’t smaller, nothing changes. Here 0 → 2 → 4 already costs 8, so the route through 1 (3 + 6 = 9) is ignored.
  7. When an entry comes out with d > dist[u], it’s stale: a shorter path was found after it was pushed. Skip it. This is called lazy deletion, and it’s simpler than updating entries inside the heap.
  8. When the heap is empty, every reachable node is settled. Nodes still at ∞ are unreachable and get -1. The edges that set each node’s final distance form the shortest-path tree.
loading dijkstra…

Why it’s correct: when the heap gives you the smallest d still waiting, every other unsettled node is at least d away. Any other route to u must pass through one of them, and since edges never have negative weight, that route can only get longer from there. So d can’t be beaten, and u is safe to settle.

import heapq
def dijkstra(graph, source):
"""Shortest distance from source to every node, or -1 if unreachable.
graph[u] is a list of (v, w) edges with w >= 0."""
INF = float("inf")
dist = [INF] * len(graph)
dist[source] = 0
heap = [(0, source)]
while heap:
d, u = heapq.heappop(heap)
if d > dist[u]: # a shorter path was found later
continue
for v, w in graph[u]:
if d + w < dist[v]:
dist[v] = d + w
heapq.heappush(heap, (dist[v], v))
return [-1 if x == INF else x for x in dist]
#include <functional>
#include <limits>
#include <queue>
#include <utility>
#include <vector>
using namespace std;
using Graph = vector<vector<pair<int, long long>>>; // graph[u] = {(v, w), ...}, w >= 0
// Shortest distance from source to every node, or -1 if unreachable.
vector<long long> dijkstra(const Graph& graph, int source) {
const long long INF = numeric_limits<long long>::max();
vector<long long> dist(graph.size(), INF);
dist[source] = 0;
// min-heap of (distance, node)
priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<>> heap;
heap.push({0, source});
while (!heap.empty()) {
auto [d, u] = heap.top();
heap.pop();
if (d > dist[u]) { // a shorter path was found later
continue;
}
for (auto [v, w] : graph[u]) {
if (d + w < dist[v]) {
dist[v] = d + w;
heap.push({dist[v], v});
}
}
}
for (auto& x : dist) if (x == INF) x = -1;
return dist;
}
import java.util.*;
class Dijkstra {
// graph.get(u) holds edges {v, w} with w >= 0.
// Returns the shortest distance from source to every node, or -1 if unreachable.
static long[] shortestPaths(List<List<int[]>> graph, int source) {
long[] dist = new long[graph.size()];
Arrays.fill(dist, Long.MAX_VALUE);
dist[source] = 0;
// min-heap of {distance, node}
PriorityQueue<long[]> heap = new PriorityQueue<>(
(x, y) -> x[0] != y[0] ? Long.compare(x[0], y[0]) : Long.compare(x[1], y[1]));
heap.add(new long[] {0, source});
while (!heap.isEmpty()) {
long[] top = heap.poll();
long d = top[0];
int u = (int) top[1];
if (d > dist[u]) { // a shorter path was found later
continue;
}
for (int[] edge : graph.get(u)) {
int v = edge[0], w = edge[1];
if (d + w < dist[v]) {
dist[v] = d + w;
heap.add(new long[] {dist[v], v});
}
}
}
for (int i = 0; i < dist.length; i++) {
if (dist[i] == Long.MAX_VALUE) dist[i] = -1;
}
return dist;
}
}

A negative edge breaks that argument. Take edges S → A costing 2, S → B costing 5 and B → A costing −4. Dijkstra settles A at 2, but the real shortest path S → B → A costs 1. The textbook version, which never revisits a settled node, returns 2. The lazy code above happens to recover here by re-processing A, but on bigger graphs that re-processing can take exponential time. Either way the guarantee is gone, so use Bellman-Ford when weights can be negative.

Why it’s O((V + E) log V)

Each successful relaxation pushes one entry, so there are at most E + 1 pushes and at most E + 1 pops. A heap operation on up to E entries costs O(log E), and since E ≤ V², that’s the same as O(log V).

Each node is settled once, and only then are its edges scanned, so the edge loops do E relaxations in total. Stale entries are popped and dropped without touching any edges.

Part Count Cost each
Pushes and pops at most E + 1 O(log V)
Edge relaxations E O(1) plus a push
Total O((V + E) log V)

Space is O(V) for dist plus O(E) for the heap, which can hold several entries for the same node.

Common mistakes

Marking nodes done when they’re pushed

BFS can mark a node as visited when it first sees it. Dijkstra can’t: the first path found is often not the shortest. A node is final only when it’s popped as the smallest entry.

if v not in seen: seen.add(v); push(...) # ✗ locks in the first path
if d + w < dist[v]: dist[v] = d + w; push(...) # ✓ keep improving until popped

Dropping the stale check

Without if d > dist[u]: continue, the answers stay right, but every out-of-date entry scans its node’s edges again. On dense graphs that can turn O(E log V) into something much slower.

d, u = heappop(heap) # ✗ then relaxes from old entries too
if d > dist[u]: continue # ✓ skip entries that were beaten

Using it with negative weights

Dijkstra assumes a settled node can’t improve. One negative edge breaks that, and you get wrong distances or very slow runs with no error. Check the constraints: if weights can be negative, use Bellman-Ford.

Overflowing the distance

Ten edges of weight 10⁹ add up to more than a 32-bit int holds. In C++ and Java, store dist as 64-bit and don’t use INT_MAX as ∞ if you add weights to it.

vector<int> dist(n, INT_MAX); // ✗ d + w overflows
vector<long long> dist(n, LLONG_MAX); // ✓ and only add to finite distances

Variations

  • Path reconstruction. Store parent[v] = u whenever dist[v] improves. To get the path to a target, follow parent back to the source and reverse it.
  • Stop early at a target. If you only need one destination, return as soon as it is popped with a non-stale entry. Its distance is already final.
  • 0-1 BFS. When every weight is 0 or 1, replace the heap with a deque: push to the front for weight 0 and to the back for weight 1. That’s O(V + E).
  • Bellman-Ford handles negative weights and detects negative cycles, at O(V · E). Use it when Dijkstra’s assumption fails.
  • A* search adds an estimate of the remaining distance to each heap key, so the search heads toward the target first. With an estimate that never overshoots, it still finds the shortest path, usually after exploring far fewer nodes.

Check yourself

5 quick questions. Pick an answer to see why it's right or wrong.

  1. 1

    Routers are joined by one-way links, each with a delay of 0 ms or more. A message leaves router k. How long until every router has it (or -1 if some never do)?

  2. 2

    What does this print?

    import heapq
    graph = {"S": [("A", 6), ("B", 2)], "B": [("A", 3), ("C", 7)], "A": [("C", 1)], "C": []}
    dist, heap = {"S": 0}, [(0, "S")]
    while heap:
    d, u = heapq.heappop(heap)
    if d > dist[u]:
    continue
    for v, w in graph[u]:
    if d + w < dist.get(v, float("inf")):
    dist[v] = d + w
    heapq.heappush(heap, (d + w, v))
    print(dist["A"], dist["C"])
  3. 3

    This version marks a node as done when it’s pushed, like BFS does. What does it print?

    import heapq
    graph = {"S": [("A", 8), ("B", 1)], "B": [("A", 2)], "A": []}
    dist, seen, heap = {"S": 0}, {"S"}, [(0, "S")]
    while heap:
    d, u = heapq.heappop(heap)
    for v, w in graph[u]:
    if v not in seen: # mark nodes when they are pushed
    seen.add(v)
    dist[v] = d + w
    heapq.heappush(heap, (d + w, v))
    print(dist["A"])
  4. 4

    In the lazy-deletion version (push a new entry on every improvement, skip stale ones when popped), how many entries can the heap hold at once, and what’s the running time?

  5. 5

    Why does Dijkstra’s guarantee break when an edge has a negative weight, even with no negative cycles?

Practice problems

Further reading

esc