Graphs · Advanced Graph Algorithms

Dijkstra's Algorithm

Repeatedly expanding outward from the source to whichever unfinalized node currently has the smallest known distance, using a priority queue to always process nodes in that order - guaranteed correct only because all edge weights are non-negative, so no future discovery could ever retroactively shrink an already-finalized node's distance.

  • 5 min read
  • 10 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The approach
  3. A second flavor: other path costs
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

BFS finds the path with the fewest edges. Once edges have weights, such as road lengths, latencies or prices, the fewest edges is no longer the cheapest route. Dijkstra's algorithm finds the cheapest distance from one source to every node. It settles nodes in order of distance, always expanding the closest unsettled one next from a min-heap. It's correct as long as no edge weight is negative.

We'll go from simple to more technical:

  1. The pattern: a min-heap of (distance, node) with lazy deletion of stale entries.
  2. A second flavor: other path costs, such as the worst single step instead of the sum.
  3. Where it runs in production (routing, network paths, maps).
  4. What breaks it: negative edges, stale entries, and comparators.

The approach

Java

int[] dijkstra(int n, List<List<int[]>> adj, int src) {         // adj.get(u) holds {v, weight}
    int[] dist = new int[n];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[src] = 0;
    PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[1], b[1]));   // {node, dist}
    pq.add(new int[]{src, 0});
    while (!pq.isEmpty()) {
        int[] cur = pq.poll();
        int node = cur[0], d = cur[1];
        if (d > dist[node]) continue;                              // stale entry - a shorter one was already used
        for (int[] edge : adj.get(node)) {
            int next = edge[0], weight = edge[1];
            if (dist[node] + weight < dist[next]) {
                dist[next] = dist[node] + weight;
                pq.add(new int[]{next, dist[next]});
            }
        }
    }
    return dist;
}

Python

import heapq


def dijkstra(n: int, adj: list[list[tuple[int, int]]], src: int) -> list[float]:   # adj[u] holds (v, weight)
    dist = [float("inf")] * n
    dist[src] = 0
    heap = [(0, src)]                          # (dist, node): tuples compare by their first item
    while heap:
        d, node = heapq.heappop(heap)
        if d > dist[node]:
            continue                           # stale entry - a shorter one was already used
        for nxt, weight in adj[node]:
            if d + weight < dist[nxt]:
                dist[nxt] = d + weight
                heapq.heappush(heap, (dist[nxt], nxt))
    return dist

The heap always hands back the unsettled node with the smallest known distance. With non-negative weights, no path discovered later can be shorter - every other route leaves through a node that is at least as far away, and edges only add. So that node's distance is final, and relaxing its edges is safe. Java's PriorityQueue can't lower an existing entry's key, so the code simply pushes a new entry on every improvement and skips any popped entry that's out of date - "lazy deletion". Python's heapq has no decrease-key either, so the Python version works the same way.

A second flavor: other path costs

The argument never used the fact that path costs are sums - only that extending a path never makes it cheaper. "Effort" on a hiking grid - the largest height difference of any single step - has the same property, so the same algorithm works with max in place of +:

Java

int next = Math.max(effort, Math.abs(heights[nr][nc] - heights[r][c]));   // cost of a path = its worst step
if (next < best[nr][nc]) { best[nr][nc] = next; pq.add(new int[]{next, nr, nc}); }

Python

step = max(effort, abs(heights[nr][nc] - heights[r][c]))   # cost of a path = its worst step
if step < best[nr][nc]:
    best[nr][nc] = step
    heapq.heappush(heap, (step, nr, nc))

Bottleneck routes (maximize the minimum capacity), minimum-effort paths and "latest possible departure" problems are all Dijkstra with a different combine step. Swapping max back to + answers a different question - the total climb, not the worst step.

Where you'll see it

Link-state routing protocols (OSPF, IS-IS) run Dijkstra on each router's view of the network to build forwarding tables. Map and navigation services start from Dijkstra and add heuristics and precomputation (A*, contraction hierarchies) to answer queries on continent-sized road graphs in milliseconds.

Which tool for which job?

You needUse
Cheapest paths from one source, weights ≥ 0Dijkstra with a binary heap and lazy deletion
One source to one targetDijkstra that stops when the target is popped
One target with a good distance estimateA* (distance so far plus a never-overestimating estimate)
All weights equalBFS
Weights only 0 or 10-1 BFS with a deque
Negative weightsBellman-Ford
A path's worst step or bottleneckDijkstra with max or min in place of +

Common mistakes

MistakeWhat happensDo this instead
Negative edge weightsa settled node later gets a cheaper route (1 instead of -5)Bellman-Ford
No stale-entry checkoutdated entries re-relax every edgeskip if the popped distance > dist[u]
int path sums in Javaa sum overflows negative and wins (-1294967296 instead of 5)long distances
Stopping when the target is first relaxeda longer route is returned (10 instead of 3)stop when the target is popped
(node, dist) tuples in Python's heapqpops by node label, re-expanding nodes(dist, node)

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. Edges 0->1 (1), 0->2 (5), 2->1 (-10). What does a finalize-once Dijkstra give for node 1, and why?

Show answer

1 instead of -5. Node 1 (distance 1) is popped before node 2 (distance 5) and treated as final; when 2 is processed later, the cheaper route to 1 is ignored. The greedy "closest is final" step assumes no edge can make a later route cheaper - exactly what a negative edge does.

2. Why can a node appear in the heap several times, and what stops that from causing wrong answers?

Show answer

Every time a node's distance improves, a new entry is pushed (Java's PriorityQueue can't update an existing one). When an older entry is popped, its distance is larger than dist[node], so the d > dist[node] check skips it. The best entry is always popped first anyway.

3. On a height grid, the effort of a path is its largest single height step. Why does Dijkstra still find the minimum-effort path?

Show answer

Dijkstra's correctness only needs path costs that never decrease when a path is extended. The maximum of a path's steps can only stay the same or grow as steps are added, so the node popped with the smallest effort is final - the same argument as with sums.

Key takeaways

  • Dijkstra settles nodes in order of distance, using a min-heap; with non-negative weights, a popped node's distance is final.
  • Push a new entry on every improvement and skip stale entries when popped (lazy deletion).
  • Negative edges break the greedy argument - finalize-once versions go wrong, lazy versions can blow up; use Bellman-Ford.
  • Any path cost that never shrinks when extended works: sums, maxima, bottlenecks.
  • O((V + E) log V); unit weights reduce to BFS.

Quick revise

The whole skill on one page, for the night before an interview.

This page puts the lesson and the deep dive together. Skim it before practice or an interview, and go back to the full lesson for anything you don't recognize.

Pick the tool

You needUse
Weights ≥ 0, one sourceDijkstra (binary heap, lazy deletion)
One targetstop when it's popped
A good estimateA*
Equal weights / 0-1 weightsBFS / 0-1 BFS
Negative weightsBellman-Ford

Snippets worth knowing by heart

Java

long[] dist = new long[n];
Arrays.fill(dist, Long.MAX_VALUE);
dist[src] = 0;
PriorityQueue<long[]> pq = new PriorityQueue<>((a, b) -> Long.compare(a[0], b[0]));
pq.add(new long[] {0, src});
while (!pq.isEmpty()) {
    long[] top = pq.poll(); int u = (int) top[1];
    if (top[0] > dist[u]) continue;                       // stale entry
    for (int[] e : adj.get(u))
        if (dist[u] + e[1] < dist[e[0]]) { dist[e[0]] = dist[u] + e[1]; pq.add(new long[] {dist[e[0]], e[0]}); }
}

Python

heap = [(0, src)]                        # (distance, node): distance first
while heap:
    d, u = heapq.heappop(heap)
    if d > dist[u]:
        continue
    if u == target:
        return d                         # final once popped
    for v, w in adj[u]:
        if d + w < dist[v]:
            dist[v] = d + w
            heapq.heappush(heap, (d + w, v))

The traps

  • Negative edges: a settled node later gets a cheaper route. Use Bellman-Ford.
  • No stale check: outdated heap entries re-relax every edge. Skip a pop whose distance exceeds dist[u].
  • int sums in Java: an overflowed sum goes negative and wins. Keep distances in long.
  • Stopping at the first relaxation: a longer route is returned. Stop when the target is popped.
  • (node, dist) tuples in heapq: it pops by node label, not distance. Push (dist, node).

Staff-level points, one line each

  • 200 x 200 grid: run to the end, 40,000 settled; stop at the target, 5,956; A*, 4,684. All gave 345.
  • Stopping at the first relaxation returned 10 when the true distance was 3.
  • Lazy deletion on 20,000 nodes / 100,000 edges: 28,747 pushes and 8,909 stale pops.
  • int sums: 1.5e9 + 1.5e9 became -1294967296 and beat the real answer of 5.
  • Dense graphs: an O(V²) array scan with no heap is often fastest.

Say it in an interview

  • "Popped means final, because weights are non-negative."
  • "No decrease-key, so I push duplicates and skip stale ones: O((V + E) log V)."
  • "For one target I stop when it's popped, and with a good estimate I'd use A*."
  • "Distances go in a long."

Practice

This skill comes with 10 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Dijkstra's Algorithm free

Dijkstra's Algorithm

MediumProCoding problemJava · Python

Given an undirected weighted graph with n nodes (labeled 0 to n-1), non-negative edge weights, and a source node, return the shortest distance from the source to every node. Use -1 for a node unreachable from the source.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Path With Minimum Effort

MediumProCoding problemJava · Python

Each cell of an R x C grid has a height. You walk from the top-left cell to the bottom-right cell, moving up, down, left or right. A route's effort is the biggest height change between two neighboring cells on it. Return the smallest effort any route can get away with.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Dijkstra's Algorithm free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime