Pattern 17 of 24 · Graphs · 35 min

Shortest Paths: BFS, Dijkstra, Bellman-Ford

BFS for unweighted graphs, Dijkstra with a heap for non-negative weights, and Bellman-Ford when the number of edges is limited.

Spot it:minimum cost / time / effortweighted edgesat most k stopsnetwork delayJavaScript you'll use: Heaps and priority queues Graphs and grids in JavaScript
Say it in 30 seconds
  • O(E log V)Dijkstra with a heap, non-negative weights
  • O(V · E)Bellman-Ford, which allows negative edges
  • O(V + E)BFS when every edge costs the same
Show what to say

“Every shortest-path algorithm repeats one move, relaxation, and only the order differs. For equal edges I use BFS, for non-negative weights Dijkstra with a min-heap, and for negative weights or an at-most-k-stops limit Bellman-Ford. Dijkstra is O(E log V), and a node is final only when it is popped.”

Trade-offs

  • Dijkstra vs Bellman-Ford: O(E log V) but needs non-negative weights, against O(V · E) that handles negative edges and stop limits.
  • Heap vs array scan for Dijkstra: the heap is O(E log V); scanning for the minimum is O(V²) and better when the graph is dense.
  • Floyd–Warshall vs Dijkstra from every node: O(V³) with negative edges fine, against O(V · E log V) for large, sparse, non-negative graphs.

Bring it up before they ask

Only a pop makes a node final in Dijkstra: skip stale entries (d > dist[u]) and stop when the target is popped, not when it is pushed.

0 of 0 sections ticked
30-second answer

Every shortest-path algorithm repeats one move, relaxation: if going through u is cheaper, lower dist[v]. Only the order differs: BFS for equal edges, Dijkstra for non-negative weights, Bellman-Ford for negative ones.

When to reach for it

Spot it in the problem0 of 5

Tap the signals you can see in the problem.

RememberMinimumWeightedFewest movesk stopslargest

Pick your variant
  • BFS (queue)

    Edge weightsall equal

    word ladder
  • Dijkstra (min-heap)

    Edge weightsnon-negative

    network delay
  • Bellman-Ford (rounds)

    Edge weightsany

    flights within k stops

The core idea: relax, closest first

Picture a road trip. Every town has a sign: "cheapest known cost to get here".

Read more · 153 words

That sign is dist[v], Infinity everywhere except the start. To relax u → v with weight w, ask: is dist[u] + w less than dist[v]? If so, fix the sign.

Dijkstra adds one rule: always visit the town with the smallest sign next. Once it is picked, its sign is final. Any other route is already at least as long, and edges never subtract.

JavaScript has no decrease-key heap, so push a fresh [dist, v] on each improvement and skip old entries when they surface (d > dist[u]). Watch B, D, E and F get stale entries:

Why negative edges break Dijkstra

Take A → B costing 4, A → C 5 and C → B -3. Dijkstra settles B at 4, but A → C → B costs 2. Its rule needs edges that never make a route cheaper, so use Bellman-Ford. With a negative cycle, no shortest path exists: one more lap is always cheaper.

Dijkstra: always settle the closest node

graph (badge = dist)
412158361A0B∞C∞D∞E∞F∞
heap [dist, node] (1)
0,A
heap
1

Every distance starts at ∞ except the source A, which is 0. The heap holds one entry, [0, A]; the smallest entry is always at the front.

function dijkstra(adj, source) {
  const dist = new Array(adj.length).fill(Infinity);
  dist[source] = 0;
  const heap = new MinHeap();
  heap.push([0, source]);
  while (heap.size > 0) {
    const [d, u] = heap.pop();
    if (d > dist[u]) continue;
    for (const [v, w] of adj[u]) {
      if (d + w < dist[v]) {
        dist[v] = d + w;
        heap.push([dist[v], v]);
      }
    }
  }
  return dist;
}
All steps as text
  1. Every distance starts at ∞ except the source A, which is 0. The heap holds one entry, [0, A]; the smallest entry is always at the front.
  2. Pop [0, A]: the source is settled at distance 0. Now relax each of its edges.
  3. A→B: 0 + 4 = 4, better than ∞. Record it and push [4, B].
  4. A→C: 0 + 1 = 1, better than ∞. Record it and push [1, C].
  5. Pop [1, C], the smallest distance waiting. Any other route to C would leave through a node that is at least 1 away and then add a non-negative edge, so 1 is final.
  6. C→B: 1 + 2 = 3 beats 4. Push [3, B]; the old [4, B] stays in the heap and will be skipped later.
  7. C→D: 1 + 5 = 6, better than ∞. Record it and push [6, D].
  8. C→E: 1 + 8 = 9, better than ∞. Record it and push [9, E].
  9. Pop [3, B], the smallest distance waiting. Any other route to B would leave through a node that is at least 3 away and then add a non-negative edge, so 3 is final.
  10. B→D: 3 + 1 = 4 beats 6. Push [4, D]; the old [6, D] stays in the heap and will be skipped later.
  11. Pop [4, B]. It is stale: B was already settled at 3, which is smaller. This leftover was pushed before the better route was found, so skip it.
  12. Pop [4, D], the smallest distance waiting. Any other route to D would leave through a node that is at least 4 away and then add a non-negative edge, so 4 is final.
  13. D→E: 4 + 3 = 7 beats 9. Push [7, E]; the old [9, E] stays in the heap and will be skipped later.
  14. D→F: 4 + 6 = 10, better than ∞. Record it and push [10, F].
  15. Pop [6, D]. It is stale: D was already settled at 4, which is smaller. This leftover was pushed before the better route was found, so skip it.
  16. Pop [7, E], the smallest distance waiting. Any other route to E would leave through a node that is at least 7 away and then add a non-negative edge, so 7 is final.
  17. E→F: 7 + 1 = 8 beats 10. Push [8, F]; the old [10, F] stays in the heap and will be skipped later.
  18. Pop [8, F], the smallest distance waiting. Any other route to F would leave through a node that is at least 8 away and then add a non-negative edge, so 8 is final.
  19. Pop [9, E]. It is stale: E was already settled at 7, which is smaller. This leftover was pushed before the better route was found, so skip it.
  20. Pop [10, F]. It is stale: F was already settled at 8, which is smaller. This leftover was pushed before the better route was found, so skip it.
  21. The heap is empty. The badges are the final distances, and the highlighted edges form the shortest-path tree (how each node was reached).

Play with it

Settle the closest town, one step at a time, or pick You pick and be the heap.

Read more · 91 words

Then stretch B–D to 15.

Step by step

  1. Model the graph

    Nodes are cities, cells or states. Build adj[u] as [v, w] pairs.

  2. Pick the algorithm from the weights

    Equal: BFS. Non-negative: Dijkstra. Negative, or an edge limit: Bellman-Ford.

  3. Define how a route's cost combines

    Usually dist[u] + w. For effort, Math.max(dist[u], step).

  4. Initialise

    dist full of Infinity, dist[source] = 0.

  5. Relax until nothing changes

    Pop, skip stale, relax, push. Stop when the target is popped.

  6. Read the answer

    dist[target], or -1 if still Infinity.

Interactive

Dijkstra's road trip

Six towns, ten two-way roads. Watch Dijkstra settle the closest town first, or be the heap yourself. Change a road and the route reroutes.

Start A · goal F (outer ring) · filled = settled · dashed = in the heap

421581036112A0B∞C∞D∞E∞F∞Min-heap, closest first0, A

Road checks 0 · heap pops 0 (0 stale) · brute force: 17 routes A to F

A starts at 0, everything else at ∞. Press Step: Dijkstra pops the closest entry from the heap.

Change road lengths

Tip: click a length on the map to pick its road.

Templates

Template 1: BFS for unweighted graphs

When every edge costs 1, a plain queue serves nodes in distance order. Mark a node when you push it.

Grids work too: Shortest Path in a Binary MatrixMedium. More in Graph BFS and DFS.

// Template 1: BFS for unweighted graphs (every edge costs 1). Nodes leave the queue in order of
// distance, so the first time a node is reached is along a shortest path.
export function bfsDistances(n, edges, source) {
  const adj = Array.from({ length: n }, () => []);
  for (const [u, v] of edges) {
    adj[u].push(v);
    adj[v].push(u);                     // undirected: drop this line for a directed graph
  }
  const dist = new Array(n).fill(-1);   // -1 = not reached (yet)
  dist[source] = 0;
  const queue = [source];
  for (let head = 0; head < queue.length; head++) { // a head index: shift() is O(n)
    const u = queue[head];
    for (const v of adj[u]) {
      if (dist[v] !== -1) continue;
      dist[v] = dist[u] + 1;            // mark when pushed so nothing is queued twice
      queue.push(v);
    }
  }
  return dist;
}

Template 2: Dijkstra with a min-heap

Keep the unvisited towns in a min-heap, so the closest is always on top. Entries are [distance, node].

A heap is a pyramid where the winner always sits at the top, kept in a plain row underneath.
import { MinHeap } from './min-heap.js';

// Template 2: Dijkstra for non-negative weights. Always expand the closest node not yet settled.
// Lazy deletion: instead of updating an entry inside the heap, push a new one and skip
// entries that are stale (a shorter distance was found after they were pushed).
export function dijkstra(n, edges, source) {
  const adj = Array.from({ length: n }, () => []);
  for (const [u, v, w] of edges) adj[u].push([v, w]); // directed; also push [u, w] onto adj[v] if undirected
  const dist = new Array(n).fill(Infinity);
  dist[source] = 0;
  const heap = new MinHeap();
  heap.push([0, source]);
  while (heap.size > 0) {
    const [d, u] = heap.pop();
    if (d > dist[u]) continue;            // stale entry: u was settled with a smaller distance
    for (const [v, w] of adj[u]) {
      if (d + w < dist[v]) {
        dist[v] = d + w;
        heap.push([dist[v], v]);
      }
    }
  }
  return dist;                            // Infinity = unreachable
}

// The same loop with two extras: remember how each node was reached (prev) to rebuild the path,
// and stop as soon as the target is popped, because a popped node's distance is final.
export function shortestPath(n, edges, source, target) {
  const adj = Array.from({ length: n }, () => []);
  for (const [u, v, w] of edges) adj[u].push([v, w]);
  const dist = new Array(n).fill(Infinity);
  const prev = new Array(n).fill(-1);
  dist[source] = 0;
  const heap = new MinHeap();
  heap.push([0, source]);
  while (heap.size > 0) {
    const [d, u] = heap.pop();
    if (d > dist[u]) continue;
    if (u === target) break;
    for (const [v, w] of adj[u]) {
      if (d + w < dist[v]) {
        dist[v] = d + w;
        prev[v] = u;
        heap.push([dist[v], v]);
      }
    }
  }
  if (dist[target] === Infinity) return { dist: -1, path: [] };
  const path = [];
  for (let v = target; v !== -1; v = prev[v]) path.push(v);
  return { dist: dist[target], path: path.reverse() };
}

Template 3: Bellman-Ford, round by round

Each round relaxes every edge, reading last round's distances and writing a copy.

Read more · 38 words

After round r, dist[v] is the cheapest path with at most r edges. So n - 1 rounds suffice.

That cracks Cheapest Flights Within K StopsMedium: k stops is k + 1 rounds. Updating in place would chain several flights in one round.

Bellman-Ford: one round per extra edge

graph (badge = dist)
45-326A0B∞C∞D∞
distances (1×4)
ABCD
dist0∞∞∞
round
0

Only the source A is reachable with 0 edges: dist = 0 for A and ∞ for everyone else. We will run 3 rounds (n - 1 = 3 rounds: enough for any shortest path).

function bellmanFord(n, edges, source, rounds) {
  let dist = new Array(n).fill(Infinity);
  dist[source] = 0;
  for (let r = 1; r <= rounds; r++) {
    const next = dist.slice();
    for (const [u, v, w] of edges) {
      if (dist[u] + w < next[v]) {
        next[v] = dist[u] + w;
      }
    }
    dist = next;
  }
  return dist;
}
All steps as text
  1. Only the source A is reachable with 0 edges: dist = 0 for A and ∞ for everyone else. We will run 3 rounds (n - 1 = 3 rounds: enough for any shortest path).
  2. Round 1: copy dist into next. Every edge reads from dist (paths with at most 0 edges) and writes into next, so this round adds at most one edge to any path.
  3. A→B (4): 0 + 4 = 4 beats ∞, so next[B] = 4.
  4. A→C (5): 0 + 5 = 5 beats ∞, so next[C] = 5.
  5. C→B (-3): C isn't reachable yet in dist (∞), so this edge can't help this round.
  6. B→D (2): B isn't reachable yet in dist (∞), so this edge can't help this round.
  7. C→D (6): C isn't reachable yet in dist (∞), so this edge can't help this round.
  8. End of round 1: next becomes dist. These are now the best costs using at most 1 edge.
  9. Round 2: copy dist into next. Every edge reads from dist (paths with at most 1 edge) and writes into next, so this round adds at most one edge to any path.
  10. A→B (4): 0 + 4 = 4 doesn't beat next[B] = 4.
  11. A→C (5): 0 + 5 = 5 doesn't beat next[C] = 5.
  12. C→B (-3): 5 + (-3) = 2 beats 4, so next[B] = 2.
  13. B→D (2): 4 + 2 = 6 beats ∞, so next[D] = 6.
  14. C→D (6): 5 + 6 = 11 doesn't beat next[D] = 6.
  15. End of round 2: next becomes dist. These are now the best costs using at most 2 edges.
  16. Round 3: copy dist into next. Every edge reads from dist (paths with at most 2 edges) and writes into next, so this round adds at most one edge to any path.
  17. A→B (4): 0 + 4 = 4 doesn't beat next[B] = 2.
  18. A→C (5): 0 + 5 = 5 doesn't beat next[C] = 5.
  19. C→B (-3): 5 + (-3) = 2 doesn't beat next[B] = 2.
  20. B→D (2): 2 + 2 = 4 beats 6, so next[D] = 4.
  21. C→D (6): 5 + 6 = 11 doesn't beat next[D] = 4.
  22. End of round 3: next becomes dist. These are now the best costs using at most 3 edges.
  23. Return dist. B = 2 comes through the negative edge C→B. Dijkstra would have settled B at 4 before it ever expanded C.
// Template 3: Bellman-Ford. Each round relaxes every edge once, reading the previous round's
// distances, so after r rounds dist[v] is the shortest path that uses at most r edges.
// Negative weights are fine. Pass maxEdges = k + 1 for "at most k stops".
export function bellmanFord(n, edges, source, maxEdges = n - 1) {
  let dist = new Array(n).fill(Infinity);
  dist[source] = 0;
  for (let round = 1; round <= maxEdges; round++) {
    const next = dist.slice();            // read from dist, write into next
    let changed = false;
    for (const [u, v, w] of edges) {
      if (dist[u] + w < next[v]) {        // Infinity + w stays Infinity, so unreached nodes relax nothing
        next[v] = dist[u] + w;
        changed = true;
      }
    }
    dist = next;
    if (!changed) break;                  // nothing improved, so later rounds can't either
  }
  return dist;
}

// After n - 1 rounds every shortest path is final. If an edge can still be relaxed,
// a negative cycle is reachable and "shortest" is undefined.
export function hasNegativeCycle(n, edges, source) {
  const dist = bellmanFord(n, edges, source);
  return edges.some(([u, v, w]) => dist[u] + w < dist[v]);
}

Try it

try-it.js
// Edit the graphs and press Run. The functions are the templates above.
console.log(bfsDistances(5, [[0, 1], [0, 2], [1, 3], [2, 3], [3, 4]], 0)); // [0, 1, 1, 2, 3]

const roads = [[0, 1, 4], [0, 2, 1], [2, 1, 2], [1, 3, 1], [2, 3, 5], [3, 4, 3]];
console.log(dijkstra(5, roads, 0));            // [0, 3, 1, 4, 7]
console.log(shortestPath(5, roads, 0, 4));     // { dist: 7, path: [0, 2, 1, 3, 4] }

// A negative edge: node 1 is popped at distance 2 before 0 → 2 → 1 (cost 1) is found.
const tricky = [[0, 1, 2], [0, 2, 5], [2, 1, -4]];
console.log(shortestPath(3, tricky, 0, 1).dist); // 2 (wrong)
console.log(bellmanFord(3, tricky, 0));          // [0, 1, 5]

// At most 2 edges: the 3-edge route to node 3 is not allowed.
const flights = [[0, 1, 1], [1, 2, 1], [2, 3, 1], [0, 2, 5], [0, 3, 9]];
console.log(bellmanFord(4, flights, 0, 2));      // [0, 1, 2, 6]

const heap = new MinHeap();
[[5, 'e'], [1, 'a'], [3, 'c']].forEach((pair) => heap.push(pair));
console.log(heap.pop(), heap.pop());             // [1, 'a'] [3, 'c']

Complexity

Read more · 45 words
AlgorithmTimeSpace
BFSO(V + E)O(V)
Dijkstra with a heapO(E log V)O(V + E)
Bellman-FordO(V · E)O(V)

Dijkstra with an array scan is O(V²), better on dense graphs. Floyd–Warshall (all pairs) is O(V³).

JavaScript you'll use

JavaScript refresher: Heaps and priority queues Graphs and grids in JavaScript Stacks, queues and deques
Read more · 38 words

The real trap is the missing priority queue. Re-sorting on every push, or queue.shift(), wrecks the complexity.

const adj = Array.from({ length: n }, () => []);   // one fresh list per node
for (const [u, v, w] of edges) adj[u].push([v, w]);
const dist = new Array(n).fill(Infinity);           // Infinity + w is still Infinity
const [d, u] = heap.pop();                          // entries are [distance, node]
const next = dist.slice();                          // Bellman-Ford: read old, write new
const DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];    // grid neighbours

Interview questions

How do you return the path itself, not just its cost?

Set prev[v] = u on every improvement. Walk prev back from the target and reverse.

Edges cost either 0 or 1. Can you beat Dijkstra?

Yes: 0-1 BFS puts 0-edges at the front of a deque and 1-edges at the back. No heap, O(V + E).

How would you detect a negative cycle?

Run one more Bellman-Ford pass after n - 1 rounds. An edge that still relaxes means a negative cycle.

You need shortest paths between every pair. What would you do?

Small and dense: Floyd–Warshall, O(V³). Large and sparse: Dijkstra from every node.

Dijkstra is too slow for one A-to-B query on a huge map. What now?

Stop when B is popped, search from both ends, or use A*, which orders the heap by dist[v] plus a straight-line guess to B.

Check yourself

Question 1 of 3

Score 2 out of 3 or better to mark this page complete.

1. Dijkstra pops [7, E] from the heap, but dist[E] is already 5. What should it do, and why?

Practice problems

Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.

Open these in the problem list →

Next: Backtracking explores every choice, undoing each one before trying the next.