# Dijkstra's Algorithm

> Shortest paths in a weighted network, one node at a time. Why the closest unfinished node is always final, how a heap finds it, and why a negative edge breaks the idea.

- Track: DSA (Concepts)
- Level: Intermediate
- Format: Step-through player
- Author: Aayush Ostwal
- Published: 26 October 2026
- Topics: Graphs, Shortest path, Heap
- Canonical URL: https://compoundengineer.com/library/dsa/dijkstras-algorithm

Concept · Shortest path

Intermediate · O((V + E) log V) time · O(V + E) space

## Why it exists

A map has towns joined by roads, and each road has a length. You want the shortest route from one town to every other. Breadth-first search counts roads, not kilometres, so it picks the route with the fewest roads, which can be far longer.

Trying every route works but explodes: the number of routes grows exponentially with the number of roads. Repeating a search from each town with a "best so far" check is better but still wastes work, because the same town is reached again and again by routes we already know are worse.

We only need one number per town: the length of its shortest route. Which town's number can we be sure of right now?

## The core idea

Keep a tentative distance for every town: the shortest route found so far. It starts at ∞ for every town except the start, which is 0. Then repeat one move:

Take the unfinished town with the smallest tentative distance. Declare it final . Then look at its roads. For each neighbour, check whether going through this town is shorter than what the neighbour has. If it is, update the neighbour. This check is called a relaxation .

The unfinished town with the smallest distance is already final, because every other route to it must pass through a town that is at least as far away.

A min-heap makes "take the smallest" cheap. We push every improved distance into it as a (distance, town) pair.

## A worked example

Five towns, six roads, and the start is town 0. The number beside a town is its tentative distance. The number on a road is its length.

Start. Town 0 is final at 0. Its roads give town 1 a distance of 4 and town 2 a distance of 1.

An improvement. The smallest unfinished town is 2, at distance 1. Through it, town 1 costs 1 + 2 = 3, which beats the 4 it had. The distance of town 1 drops to 3, and a new entry goes into the heap.

A stale entry. The old entry (4, town 1) is still in the heap. When it comes out, town 1 already has 3, so the entry is stale and we throw it away. The same happens to (9, town 3) later, after town 3 improves to 8.

Done. Every town is popped once with its final distance: 0, 3, 1, 8 and 11. The roads that gave each town its distance form the shortest-path tree.

## How it works

- Set every distance to ∞ except the start, which is 0. Push (0, start).

- Pop the smallest (d, u). d is larger than dist[u]⟶stale, skip it

- For each road (u, v, w): d + w Say we pop town u at distance d . Suppose a shorter route to u existed. It starts in the finished area and has to leave it through some road to an unfinished town x . That town is still waiting in the heap, so its distance is at least d , because we popped the smallest. The rest of the route from x to u can only add length, so the whole route is at least d . No shorter route exists, and d is final.

### Why edges cannot be negative

The step "the rest of the route can only add length" is where the proof needs non-negative roads. With a negative road, a long first leg followed by a big negative one could beat a town we already finalised. Dijkstra would then return a wrong answer without any error.

## Watch it run

Pick an input, then press Play or use the arrow keys. The highlighted line is the one running, and the number beside each line counts how often it ran. The graph shows the tentative distance by each town and draws the current shortest-path tree. The heap on the right holds the pending entries, smallest on top.

## Complexity

O((V + E) log V)

Time. Here's why: each road can improve a distance at most once per direction, so at most 2E entries are ever pushed, and each push or pop on a heap costs O(log V). A pop that finds a stale entry costs O(log V) and nothing more. Each town is expanded once, which scans its roads for O(V + E) in total.

O(V + E)

Space. The adjacency lists hold V lists and 2E entries. dist holds V numbers. The heap holds at most 2E entries, because stale entries stay until they are popped.

## Mistakes and variants

- Forgetting the stale check. Without if d > dist[u]: continue, a town is expanded once for every entry it has. The answer is still correct, but the running time can grow badly.

- Negative edges. Dijkstra can silently return wrong distances. Use Bellman-Ford instead, which allows negative edges and costs O(V × E).

- Stopping early. If you only need the distance to one target, return as soon as that target is popped. Its distance is final at that moment.

- A different cost. Only the "add" step changes the meaning. Swapping d + w for max(d, w) finds the route whose worst road is smallest, which is how grid problems like Swim in Rising Water use it.

- A plain array instead of a heap. Scanning an array for the smallest unfinished town costs O(V) per pick, or O(V²) overall. That beats a heap only on very dense graphs.

## Where it shows up

Reach for it when:

- roads, edges or steps have different non-negative costs, and you want the cheapest route;

- "fewest steps" would be BFS, but each step costs something different;

- the cost of a path is not a sum but a worst step (a maximum), and the same greedy idea still applies.

Problems that use it:

- Swim in Rising Water

## Interviewer follow-ups

These are the questions that usually come next. Open one to see an answer you could say out loud.

"What if one road has a negative length?"

What they are checking Whether you know why the algorithm works, not only the steps. They want to see you point at the exact line of the proof that breaks.

A strong answer The proof says a route through an unfinished town can only be longer. A negative road breaks that, so a town we finalised could later get a shorter route and Dijkstra would not notice. I would use Bellman-Ford, which is O(V × E), and it can also report a negative cycle. If the graph has no cycles, a topological order is faster.

"The graph is almost fully connected. Is a heap still the right choice?"

What they are checking Whether you tie the data structure to the shape of the input instead of using the same answer every time.

A strong answer With E close to V², the heap costs about V² log V. Scanning an array for the smallest unfinished town costs O(V) per pick and O(V²) overall, with no heap overhead. So on a dense graph the plain array is better. On a sparse graph, where E is near V, the heap is better.

"I only need the route to one town, in a graph with millions of towns. What do you change?"

What they are checking Whether you can stop work you do not need, and whether you can return the route itself instead of only its length.

A strong answer Stop as soon as the target is popped, since its distance is final then. To return the route, record parent[v] = u each time a distance improves, and walk back from the target. It still explores everything closer than the target, so if the data allows a good estimate of the remaining distance, A* uses that to explore far fewer towns.

## Solution code

### Python

```python
import heapq

class Dijkstra:
    def shortestPaths(self, n, edges, src):
        graph = [[] for _ in range(n)]
        for u, v, w in edges:
            graph[u].append((v, w))
            graph[v].append((u, w))
        dist = [float("inf")] * n
        dist[src] = 0
        heap = [(0, src)]  # (distance, node)
        while heap:
            d, u = heapq.heappop(heap)  # smallest is final
            if d > dist[u]:
                continue  # stale entry, skip it
            for v, w in graph[u]:
                if d + w < dist[v]:
                    dist[v] = d + w
                    heapq.heappush(heap, (d + w, v))
        return dist
`.replace(/^\n/, '');

function solve(n, edges, src) {
  const g = Array.from({ length: n }, () => []);
  edges.forEach(([u, v, w]) => { g[u].push([v, w]); g[v].push([u, w]); });
  const dist = Array(n).fill(Infinity); dist[src] = 0;
  const h = [[0, src]];
  while (h.length) {
    h.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
    const [d, u] = h.shift();
    if (d > dist[u]) continue;
    for (const [v, w] of g[u]) if (d + w < dist[v]) { dist[v] = d + w; h.push([d + w, v]); }
  }
  return dist;
}

function traceDijkstra(T, n, edgesIn, src) {
  const pos = POS[n];
  const nodes = [...Array(n).keys()].map(i => pos ? { id: i, x: pos[i][0], y: pos[i][1] } : { id: i });
  const edges = edgesIn.map(([u, v, w]) => ({ u, v, w }));
  const adj = Array.from({ length: n }, () => []);
  edgesIn.forEach(([u, v, w]) => { adj[u].push([v, w]); adj[v].push([u, w]); });
  const ekey = (a, b) => { const e = edges.find(e => (e.u === a && e.v === b) || (e.u === b && e.v === a)); return `${e.u}-${e.v}
```

---
From Compound Engineer (https://compoundengineer.com), visual lessons for engineers. The page above is the full interactive version.
