~/graphs/dijkstra
Dijkstra's algorithm
Shortest paths from one source when edge weights are never negative. Always finish the closest unfinished node next.
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.
You need the cheapest route from one start node, and no edge weight is negative.
O((V + E) log V)
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.
- Set every
distto ∞, exceptdist[source] = 0. Put (0, source) in the heap. - Pop the entry with the smallest distance,
(d, u). - If
dstill equalsdist[u], thenuis settled. Go through each edge fromuto a neighbourvwith weightw. - Relax the edge: if
d + w < dist[v], the route throughuis shorter. Setdist[v] = d + wand push(dist[v], v). - 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. - If
d + wisn’t smaller, nothing changes. Here 0 → 2 → 4 already costs 8, so the route through 1 (3 + 6 = 9) is ignored. - 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. - 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.
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] = uwheneverdist[v]improves. To get the path to a target, followparentback 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
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)?One source, non-negative weights: that’s Dijkstra, in O((V + E) log V). The time until everyone has the message is the largest shortest distance. BFS counts hops and ignores delays. Floyd-Warshall gives the right answer too, but computes all pairs in O(V³) when you only need one row. Union-find tells you who is connected, not how far.
-
2
What does this print?
import heapqgraph = {"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]:continuefor v, w in graph[u]:if d + w < dist.get(v, float("inf")):dist[v] = d + wheapq.heappush(heap, (d + w, v))print(dist["A"], dist["C"])B is settled first at 2. From B, A improves from 6 to 2 + 3 = 5, and C becomes 9. Then A is settled at 5 and C improves to 5 + 1 = 6. The tempting
6 7keeps the direct S→A edge, but the detour through B is shorter. -
3
This version marks a node as done when it’s pushed, like BFS does. What does it print?
import heapqgraph = {"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 pushedseen.add(v)dist[v] = d + wheapq.heappush(heap, (d + w, v))print(dist["A"])Popping S pushes A at 8 and B at 1, and marks both. When B is popped, the cheaper route to A (1 + 2 = 3) is ignored because A is already marked. A node’s distance is only final when it’s popped as the smallest entry, so mark it (or skip stale entries) at pop time, not push time.
-
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?
Every improvement pushes one entry and there’s no decrease-key, so a node can sit in the heap several times: up to E + 1 entries in total. Each push and pop costs O(log E), which is O(log V) because E ≤ V². The ‘one entry per node’ picture is the decrease-key version, not this one.
-
5
Why does Dijkstra’s guarantee break when an edge has a negative weight, even with no negative cycles?
The proof says: the smallest tentative distance can’t get better, because every other route is already at least that long and edges only add. A negative edge lets a longer-looking route end up shorter, so settling is no longer safe. Heaps compare negative numbers just fine. Use Bellman-Ford instead.