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.
- 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.
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
Tap the signals you can see in the problem.
RememberMinimumWeightedFewest movesk stopslargest
- 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
- 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
- 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.
- Pop [0, A]: the source is settled at distance 0. Now relax each of its edges.
- A→B: 0 + 4 = 4, better than ∞. Record it and push [4, B].
- A→C: 0 + 1 = 1, better than ∞. Record it and push [1, C].
- 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.
- C→B: 1 + 2 = 3 beats 4. Push [3, B]; the old [4, B] stays in the heap and will be skipped later.
- C→D: 1 + 5 = 6, better than ∞. Record it and push [6, D].
- C→E: 1 + 8 = 9, better than ∞. Record it and push [9, E].
- 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.
- B→D: 3 + 1 = 4 beats 6. Push [4, D]; the old [6, D] stays in the heap and will be skipped later.
- 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.
- 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.
- D→E: 4 + 3 = 7 beats 9. Push [7, E]; the old [9, E] stays in the heap and will be skipped later.
- D→F: 4 + 6 = 10, better than ∞. Record it and push [10, F].
- 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.
- 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.
- E→F: 7 + 1 = 8 beats 10. Push [8, F]; the old [10, F] stays in the heap and will be skipped later.
- 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.
- 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.
- 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.
- 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
Model the graph
Nodes are cities, cells or states. Build
adj[u]as[v, w]pairs.Pick the algorithm from the weights
Equal: BFS. Non-negative: Dijkstra. Negative, or an edge limit: Bellman-Ford.
Define how a route's cost combines
Usually
dist[u] + w. For effort,Math.max(dist[u], step).Initialise
distfull ofInfinity,dist[source] = 0.Relax until nothing changes
Pop, skip stale, relax, push. Stop when the target is popped.
Read the answer
dist[target], or-1if stillInfinity.
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].
- Network Delay TimeMedium: the template as is; answer the largest
dist. - Path With Minimum EffortMedium: relax with
Math.max(effort, step). - Swim in Rising WaterHard: same idea, cell height as cost.
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
- 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
- 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).
- 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.
- A→B (4): 0 + 4 = 4 beats ∞, so next[B] = 4.
- A→C (5): 0 + 5 = 5 beats ∞, so next[C] = 5.
- C→B (-3): C isn't reachable yet in dist (∞), so this edge can't help this round.
- B→D (2): B isn't reachable yet in dist (∞), so this edge can't help this round.
- C→D (6): C isn't reachable yet in dist (∞), so this edge can't help this round.
- End of round 1: next becomes dist. These are now the best costs using at most 1 edge.
- 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.
- A→B (4): 0 + 4 = 4 doesn't beat next[B] = 4.
- A→C (5): 0 + 5 = 5 doesn't beat next[C] = 5.
- C→B (-3): 5 + (-3) = 2 beats 4, so next[B] = 2.
- B→D (2): 4 + 2 = 6 beats ∞, so next[D] = 6.
- C→D (6): 5 + 6 = 11 doesn't beat next[D] = 6.
- End of round 2: next becomes dist. These are now the best costs using at most 2 edges.
- 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.
- A→B (4): 0 + 4 = 4 doesn't beat next[B] = 2.
- A→C (5): 0 + 5 = 5 doesn't beat next[C] = 5.
- C→B (-3): 5 + (-3) = 2 doesn't beat next[B] = 2.
- B→D (2): 2 + 2 = 4 beats 6, so next[D] = 4.
- C→D (6): 5 + 6 = 11 doesn't beat next[D] = 4.
- End of round 3: next becomes dist. These are now the best costs using at most 3 edges.
- 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
// 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
| Algorithm | Time | Space |
|---|---|---|
| BFS | O(V + E) | O(V) |
| Dijkstra with a heap | O(E log V) | O(V + E) |
| Bellman-Ford | O(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 dequesRead 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 neighboursInterview 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 3Score 2 out of 3 or better to mark this page complete.
Practice problems
Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.
- Shortest Path in a Binary MatrixFind the fewest cells on a path from the top-left to the bottom-right of a 0/1 grid, moving in any of 8 directions.Medium
- Network Delay TimeA signal starts at one node of a weighted directed network. How long until every node has received it?Medium
- Path With Minimum EffortWalk across a height map from corner to corner so that the steepest single step is as gentle as possible.Medium
- Cheapest Flights Within K StopsFind the cheapest route between two cities when you may change planes at most k times.Medium
- Swim in Rising WaterWater rises one unit per second over a grid of distinct heights. When can you first swim from one corner to the other?Hard