Shortest Paths: Dijkstra, Bellman-Ford, and Floyd-Warshall
BFS finds the shortest path in an unweighted graph because every edge costs the same, so the first arrival is always the shortest. The moment edges carry different weights, that guarantee breaks — a path through more edges can still be cheaper than a path through fewer, heavier ones. This lesson covers the three workhorse algorithms for weighted shortest paths, each suited to a different shape of problem.
What you’ll learn
Section titled “What you’ll learn”- Dijkstra’s algorithm: a greedy, heap-driven search for single-source shortest paths when every weight is non-negative.
- Bellman-Ford: slower but strictly more general — handles negative edge weights and can detect a negative cycle.
- Floyd-Warshall: a dynamic-programming sweep that computes shortest paths between every pair of nodes at once.
- Which one to reach for, based on graph size, edge weights, and whether you need one source or every pair.
The cue
Section titled “The cue”Visual intuition
Section titled “Visual intuition”The heap, not the graph, is where Dijkstra’s logic lives. Watch two things: the pop order (always the globally closest unfinalised node) and the stale entries that get skipped.
The direct edge A→B costs 4, but A→C→B costs 1 + 2 = 3. Because the heap always pops the closest unfinalised node, C is settled first and B's distance drops to 3 before B is popped -- so the old (4, B) entry is still in the heap and gets skipped as stale. That skip is not a bug; it is what replaces a decrease-key operation.
Dijkstra’s algorithm: greedy + a min-heap
Section titled “Dijkstra’s algorithm: greedy + a min-heap”Dijkstra keeps a min-heap of (distance, node) pairs and always expands the
closest unfinalized node next. Once a node is popped from the heap with
its true shortest distance, that distance can never improve — which is
exactly why negative weights break it: a “closest” node popped early could
later be beaten by a path through a negative edge that hasn’t been
discovered yet.
import heapq
from collections import defaultdict
def dijkstra(n, edges, source):
graph = defaultdict(list)
for u, v, w in edges:
graph[u].append((v, w))
graph[v].append((u, w)) # remove this line for a DIRECTED graph
dist = [float("inf")] * n
dist[source] = 0
heap = [(0, source)] # (distance so far, node)
while heap:
d, node = heapq.heappop(heap)
if d > dist[node]:
continue # a shorter distance was already found -- stale entry
for neighbor, weight in graph[node]:
new_dist = d + weight
if new_dist < dist[neighbor]:
dist[neighbor] = new_dist # relax the edge
heapq.heappush(heap, (new_dist, neighbor))
return dist
n = 5
edges = [(0, 1, 4), (0, 2, 1), (2, 1, 2), (1, 3, 1), (2, 3, 5), (3, 4, 3)]
print("shortest distances from 0:", dijkstra(n, edges, 0)) graph LR
N0((0)) -- "4" --> N1((1))
N0 -- "1" --> N2((2))
N2 -- "2" --> N1
N1 -- "1" --> N3((3))
N2 -- "5" --> N3
N3 -- "3" --> N4((4))
Even though the direct edge 0 -> 1 costs 4, Dijkstra finds that
0 -> 2 -> 1 only costs 1 + 2 = 3 — cheaper. Because the heap always
pops the globally closest unfinalized node next, this relaxation happens
before node 1 is ever finalized at the (wrong) distance 4.
Bellman-Ford: handles negative weights, detects negative cycles
Section titled “Bellman-Ford: handles negative weights, detects negative cycles”Instead of being greedy, Bellman-Ford brute-forces it: relax every edge,
V - 1 times. After V - 1 rounds, every shortest path (which can use at
most V - 1 edges in a graph with no negative cycle) is guaranteed correct.
One extra round that still finds an improvement means a negative cycle
exists — distances would keep shrinking forever.
def bellman_ford(n, edges, source):
dist = [float("inf")] * n
dist[source] = 0
for _ in range(n - 1): # V-1 rounds is enough for any negative-cycle-free graph
for u, v, w in edges:
if dist[u] != float("inf") and dist[u] + w < dist[v]:
dist[v] = dist[u] + w
# One more round: any further improvement means a negative cycle exists.
for u, v, w in edges:
if dist[u] != float("inf") and dist[u] + w < dist[v]:
return None, True # (distances, has_negative_cycle)
return dist, False
n = 5
# Directed, weighted edges -- Bellman-Ford handles negative weights directly.
edges = [(0, 1, 4), (0, 2, 1), (2, 1, -3), (1, 3, 2), (2, 3, 5), (3, 4, 1)]
distances, has_cycle = bellman_ford(n, edges, 0)
print("negative cycle?", has_cycle)
print("shortest distances from 0:", distances)Floyd-Warshall: all-pairs shortest paths
Section titled “Floyd-Warshall: all-pairs shortest paths”When you need the shortest distance between every pair of nodes at
once (not just from one source), Floyd-Warshall’s triple-nested loop
dynamic program does it directly. The recurrence considers, for each pair
(i, j), whether routing through an intermediate node k shortens the
known path:
def floyd_warshall(n, edges):
INF = float("inf")
dist = [[0 if i == j else INF for j in range(n)] for i in range(n)]
for u, v, w in edges:
dist[u][v] = min(dist[u][v], w) # keep the cheaper edge if there are duplicates
for k in range(n): # k = the intermediate node allowed in this pass
for i in range(n):
for j in range(n):
if dist[i][k] + dist[k][j] < dist[i][j]:
dist[i][j] = dist[i][k] + dist[k][j]
return dist
n = 4
edges = [(0, 1, 3), (0, 3, 7), (1, 2, 1), (2, 3, 2)]
all_pairs = floyd_warshall(n, edges)
for row in all_pairs:
print(row)
print("shortest 0 -> 3 via node 2:", all_pairs[0][3]) # 3 + 1 + 2 = 6, beats the direct 7Complexity
Section titled “Complexity”| Algorithm | Time | Space | Where the bound comes from |
|---|---|---|---|
| Dijkstra, binary heap | each edge can push one heap entry, so the heap holds items and | ||
| Dijkstra, Fibonacci heap | real decrease-key; theoretically better, never worth writing in an interview | ||
| Dijkstra, dense array scan | scan for the minimum instead of a heap — better when | ||
| Bellman-Ford | rounds, every edge relaxed per round | ||
| Bellman-Ford, early exit | stop when a round changes nothing; is the longest shortest-path in edges | ||
| Floyd-Warshall | three nested loops over all nodes; the matrix is the output | ||
| BFS (equal weights) | no ordering structure needed at all | ||
| 0-1 BFS (deque) | only two distances ever in flight | ||
| DAG, topological relaxation | each node is final before it is used |
Two practical notes worth saying out loud:
- rounds is not arbitrary. A shortest path visits at most nodes, so it has at most edges, and round guarantees every shortest path of edges is correct. That is also why an extra round detecting further improvement proves a negative cycle.
- Floyd-Warshall’s is fine up to roughly ( operations). Above that, run Dijkstra from each source you actually need — and if you need all pairs of a -node graph, the output is the problem, not the algorithm.
Which one to use
Section titled “Which one to use”| Algorithm | Handles negative weights? | Detects negative cycles? | Complexity | Best for |
|---|---|---|---|---|
| Dijkstra | No | No | Single source, non-negative weights, large sparse graphs | |
| Bellman-Ford | Yes | Yes | Single source, possible negative edges, or you must detect a negative cycle | |
| Floyd-Warshall | Yes | Yes (diagonal goes negative) | All-pairs shortest paths, small/dense graphs (roughly ) |
Dry run
Section titled “Dry run”Dijkstra on the undirected graph from the template —
edges = [(0,1,4), (0,2,1), (2,1,2), (1,3,1), (2,3,5), (3,4,3)], source 0.
| pop | stale? | settle at | relaxations | dist | heap after |
|---|---|---|---|---|---|
(0, 0) | no | 0 | 1→4, 2→1 | [0, 4, 1, ∞, ∞] | (1,2) (4,1) |
(1, 2) | no | 1 | 1→3 improved from 4, 3→6 | [0, 3, 1, 6, ∞] | (3,1) (4,1) (6,3) |
(3, 1) | no | 3 | 3→4 improved from 6 | [0, 3, 1, 4, ∞] | (4,1) (4,3) (6,3) |
(4, 1) | yes — dist[1] is 3 | — | skipped | unchanged | (4,3) (6,3) |
(4, 3) | no | 4 | 4→7 | [0, 3, 1, 4, 7] | (6,3) (7,4) |
(6, 3) | yes — dist[3] is 4 | — | skipped | unchanged | (7,4) |
(7, 4) | no | 7 | none | [0, 3, 1, 4, 7] | empty |
Final: [0, 3, 1, 4, 7].
Four things this makes concrete:
- Node 1 is improved twice — 4, then 3 — and settled once. The direct edge costs 4, but the route through node 2 costs 3. The heap’s ordering guarantees node 2 (distance 1) is popped before node 1 (distance 4), so the improvement lands before node 1 is ever finalised. That ordering is Dijkstra’s correctness argument.
- Two of the seven pops are stale, and skipping them is the design, not a repair.
heapqhas no decrease-key, so an improvement pushes a second, better entry; theif d > dist[node]: continueguard discards the worse one when it surfaces. The heap therefore holds up to entries, which is where the factor comes from. - A node is final the moment it is popped non-stale. Node 2 is settled at 1 and never reconsidered — nothing can beat it, because every remaining route starts from something already at distance ≥ 1 and adds a non-negative weight.
- That is exactly what a negative edge breaks. Add an edge
4 → 1of weight-9and node 1’s true distance becomes7 - 9 = -2, but node 1 was finalised at 3 long before node 4 was reached. Dijkstra does not detect this — it returns a confidently wrong answer, which is why the negative-weight rule is a hard precondition rather than a performance note. Bellman-Ford’s rounds exist precisely to keep re-relaxing.
The variant map
Section titled “The variant map”| Variant | What changes | Canonical problem |
|---|---|---|
| Plain single-source distances | the base template | LC 743 Network Delay Time |
| All weights equal | drop the heap — BFS is | LC 1091 |
| Weights are 0 and 1 | heap → deque, appendleft for 0 | 0-1 BFS · LC 1368 |
At most k edges | Bellman-Ford with exactly k+1 rounds — the round count is the edge budget, which a heap cannot express | LC 787 Cheapest Flights |
| Maximise a product of probabilities | push -prob, multiply instead of add; the “shortest” becomes the most probable | LC 1514 |
| Minimise the maximum edge (bottleneck) | relax with max(d, w) instead of d + w | LC 1631 Path With Minimum Effort |
| Extra state per node | the heap key becomes a tuple, e.g. (cost, node, fuel_left) | Dijkstra with extra state · LC 1928 |
| Count the shortest paths | keep ways[v]; on an improvement copy, on a tie add | LC 1976 |
| Reconstruct the path | store parent[v] whenever you relax, then walk back | LC 1976 |
| Negative cycle detection | one extra Bellman-Ford round: any further improvement means a negative cycle | arbitrage problems |
| All pairs, dense small graph | Floyd-Warshall, k as the outermost loop | LC 1334 |
| DAG, any weights | relax in topological order, — negatives are fine | LC 1857 |
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “Why does Dijkstra fail on negative weights?” | The central precondition | Because it finalises a node the moment it is popped, on the assumption that no later route can be cheaper — which requires non-negative weights. A negative edge discovered afterwards would beat a finalised distance, and nothing goes back to fix it |
| “What are those stale heap entries?” | Whether you understand the implementation | heapq has no decrease-key, so improving a distance pushes a second, better entry. The if d > dist[node]: continue guard drops the obsolete one. The heap can hold entries, hence |
“Why is the k in Floyd-Warshall the outermost loop?” | Whether you know why it works | Because k is the inductive parameter: after iteration k, dist[i][j] is the best path using only intermediates from . Putting k inside is a genuinely wrong algorithm that happens to be right on many test cases |
“At most k stops” (LC 787) | Recognising the shape | Bellman-Ford with k+1 rounds, relaxing from a snapshot of the previous round. The round count is the edge budget; a heap has no equivalent knob, and running Dijkstra with a hop count in the state also works but is easier to get wrong |
| “Detect currency arbitrage” | Applying the extra round | Bellman-Ford, then one more round: if anything still improves, a negative cycle is reachable. With logs and negated weights, a negative cycle is an arbitrage loop |
| “Return the path, not the distance” | Bookkeeping | Store parent[v] = u at relaxation time and walk back from the target. Recording parents on a rejected relaxation gives a path that is not shortest |
| “Minimise the largest single edge on the route” | Whether the pattern generalises | Same heap, different relaxation: new = max(d, w). Widest-path is min instead. The heap only needs the key to be monotone along a path, not additive |
| ”, all-pairs distances?” | Scale sense | Impossible — the output alone is numbers. Ask what is actually needed: one source, or a small set of sources, run Dijkstra per source |
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”Each exercise is the actual LeetCode problem with its real method signature and LeetCode’s own examples as the test. Write the body, press Run, and match the expected output.
LC 743 — Network Delay Time · Medium
Section titled “LC 743 — Network Delay Time · Medium”Problem. Given directed weighted edges times[i] = [u, v, w], n nodes and a
source k, return the time for a signal from k to reach all nodes, or -1 if
some node is unreachable.
Constraints. 1 <= k <= n <= 100, 1 <= len(times) <= 6000,
0 <= w <= 100.
Examples. times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2 gives 2 ·
times = [[1,2,1]], n = 2, k = 2 gives -1
Editorial
Dijkstra’s invariant: because all weights are non-negative, the first time a node is popped from a min-heap its distance is final — nothing still in the heap could reach it more cheaply.
Time . Space .
The if node in dist: continue line is doing real work. A node can be pushed several
times with different tentative distances; this check discards the stale ones and
replaces a separate visited set. Omitting it does not give wrong answers — the first
pop already set the correct distance — but it re-expands neighbours needlessly.
The -1 case is reachability: len(dist) < n means the signal never arrived somewhere.
([[1,2,1]], 2, 2) starts at node 2, which has no outgoing edges, so node 1 is
unreachable.
Note this requires non-negative weights. With a negative edge the first-pop invariant fails, and you need Bellman-Ford — which is the next problem’s territory.
Follow-ups: “Negative weights?” — Bellman-Ford, , and it also detects
negative cycles. “All-pairs shortest paths?” — Floyd-Warshall at , fine for
n = 100. “Why does the first pop finalise a node?” — the non-negativity argument; be
ready to state it. “Return the paths?” — store a predecessor per node.
LC 787 — Cheapest Flights Within K Stops · Medium
Section titled “LC 787 — Cheapest Flights Within K Stops · Medium”Problem. Given flights [from, to, price], find the cheapest price from src to
dst using at most k stops. Return -1 if there is no such route.
Constraints. 1 <= n <= 100, 0 <= len(flights) <= n * (n - 1) / 2,
0 <= k < n.
Examples. n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1 gives 700 ·
n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 1 gives
200, and with k = 0 gives 500
Editorial
The hop limit is what makes Dijkstra a poor fit: it finalises nodes by cost, but the cheapest way to reach a node may use too many stops to be extendable. Bellman-Ford is naturally suited, because its rounds correspond exactly to edge counts.
Time . Space .
The third case, k = 0, allows a single edge — so the direct 0 -> 2 flight at 500
beats the two-hop 200.
Dijkstra can be adapted by carrying (cost, node, stops) in the heap and allowing a
node to be revisited with fewer stops, but the state space grows and the bookkeeping is
easier to get wrong.
Follow-ups: “Why not plain Dijkstra?” — the finalisation argument above; the most
likely question. “Detect negative cycles?” — run one extra round; any further
improvement proves a negative cycle. “Exactly k stops?” — read the answer from round
k + 1 only. “Very large k?” — once k >= n - 1 the limit is irrelevant and plain
Dijkstra applies.
LC 1631 — Path With Minimum Effort · Medium
Section titled “LC 1631 — Path With Minimum Effort · Medium”Problem. Given a grid of heights, a route’s effort is the maximum absolute height difference between consecutive cells on it. Return the minimum effort to travel from the top-left to the bottom-right, moving in 4 directions.
Constraints. 1 <= rows, cols <= 100, 1 <= heights[i][j] <= 10^6.
Examples. [[1,2,2],[3,8,2],[5,3,5]] gives 2 ·
[[1,2,3],[3,8,4],[5,3,5]] gives 1 ·
a grid with a zero-difference route gives 0
Editorial
Dijkstra is usually taught with additive costs, but it works for any path cost that
is monotone non-decreasing as the path extends. max() qualifies: adding a step can
only keep the effort the same or raise it, never lower it. So the first-pop-is-final
invariant still holds.
Time . Space .
The only change from a standard Dijkstra is the relaxation:
ne = max(e, abs(height_difference)) # instead of e + weightThat generalisation — “Dijkstra needs monotonicity, not addition” — is the transferable insight, and it also covers minimax and bottleneck path problems generally.
Two alternatives worth naming:
- Binary search + BFS. Guess a threshold effort, then check with BFS/DFS whether the destination is reachable using only steps within it. That is binary search on the answer at , and many find it easier to reason about.
- Union-find. Sort all edges by difference and union them in order until the corners connect; the last edge’s difference is the answer — essentially building a minimum spanning tree, which is Kruskal’s.
[[3]] gives 0 — a single cell needs no moves.
Follow-ups: “Why does Dijkstra still work?” — the monotonicity argument; the expected question. “Do it with binary search?” — the alternative above. “With union-find?” — the Kruskal framing. “8 directions?” — extend the offsets; nothing else changes.
LeetCode problem set
Section titled “LeetCode problem set”Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.
Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 743Network Delay TimemediumDijkstra straight from the template above: how long until a signal reaches every node from one source
- 787Cheapest Flights Within K StopsmediumA Bellman-Ford variant capped at `K + 1` relaxation rounds instead of `V - 1`
- 1334Find the City With the Smallest Number of Neighbors at a Threshold DistancemediumFloyd-Warshall's all-pairs table, then count reachable cities per city
- 1631Path With Minimum EffortmediumDijkstra where the "distance" being minimized is the maximum edge weight along the path, not the sum
- 778Swim in Rising WaterhardDijkstra-style: a min-heap greedily expanding to the lowest-elevation unvisited cell, minimizing the *maximum* value on the path
Self-check
Section titled “Self-check”-
Why does a single negative edge break Dijkstra?
Note the failure mode: no crash, no loop — a confidently wrong answer. That is why non-negative weights are a precondition rather than a performance note, and why Bellman-Ford's repeated rounds exist.
pch.quizShowAnswer
B — Because Dijkstra finalises a node the moment it is popped, assuming no later route can be cheaper — a negative edge found afterwards would beat that finalised distance, and nothing revisits it — Note the failure mode: no crash, no loop — a confidently wrong answer. That is why non-negative weights are a precondition rather than a performance note, and why Bellman-Ford's repeated rounds exist.
-
Your heap pops (4, node 1) but dist[1] is already 3. What is going on?
Both stale pops in the dry run are expected. It also explains the complexity: the heap holds O(E) entries rather than O(V), giving O(E log V).
pch.quizShowAnswer
B — A stale entry: heapq has no decrease-key, so improving a distance pushes a second, better entry — the `if d > dist[node]: continue` guard discards the obsolete one — Both stale pops in the dry run are expected. It also explains the complexity: the heap holds O(E) entries rather than O(V), giving O(E log V).
-
In Floyd-Warshall, why must `k` be the outermost loop?
The wrong loop order is dangerous precisely because it passes many test cases. If you can state the invariant, you can also derive the loop order rather than memorising it.
pch.quizShowAnswer
B — Because k is the inductive parameter: after iteration k, dist[i][j] is the best path using only intermediates from {0..k}. Moving k inside is a different, wrong algorithm — The wrong loop order is dangerous precisely because it passes many test cases. If you can state the invariant, you can also derive the loop order rather than memorising it.
-
LC 787: cheapest flight with at most k stops. Why Bellman-Ford rather than Dijkstra?
Dijkstra with a hop count folded into the state also works, but it is easier to get wrong: the cheapest route to a node is not necessarily the one leaving the most hops available.
pch.quizShowAnswer
B — Because the round count IS the edge budget — k+1 rounds, relaxing from a snapshot of the previous round — and a heap has no equivalent knob — Dijkstra with a hop count folded into the state also works, but it is easier to get wrong: the cheapest route to a node is not necessarily the one leaving the most hops available.
-
The problem asks to minimise the LARGEST single edge on the route, not the total. What changes?
Widest path swaps in `min` the same way. Recognising that Dijkstra needs monotonicity rather than addition is what lets you reuse it for bottleneck, probability (LC 1514) and reliability problems.
pch.quizShowAnswer
B — The same heap with a different relaxation: `new = max(d, w)` instead of `d + w` — the key only has to be monotone along a path, not additive — Widest path swaps in `min` the same way. Recognising that Dijkstra needs monotonicity rather than addition is what lets you reuse it for bottleneck, probability (LC 1514) and reliability problems.
-
Every edge weight is 1. What should you use?
Reaching for the heavier tool when the weights are uniform is a common tell in interviews. The same reasoning one step up: weights of only 0 and 1 want a deque, not a heap.
pch.quizShowAnswer
B — BFS: O(V + E) with no ordering structure. Dijkstra is correct but pays a log factor for sorting that a queue gives free — Reaching for the heavier tool when the weights are uniform is a common tell in interviews. The same reasoning one step up: weights of only 0 and 1 want a deque, not a heap.
Recall card
Section titled “Recall card”- Cue — minimum cost/time/distance where moves cost different amounts. Equal costs → BFS; 0/1 → deque.
- Dijkstra — heap of
(dist, node); pop, skip ifd > dist[node](stale), relax neighbours, push improvements. . Non-negative weights only. - A popped non-stale node is final — that is the whole correctness argument, and exactly what a negative edge invalidates.
- Bellman-Ford — rounds over all edges, . Handles negatives; one
extra round detects a negative cycle;
k+1rounds answers “at mostkedges”. - Floyd-Warshall —
koutermost, theni,j; , good to about ; a negative value on the diagonal means a negative cycle. - DAG — relax in topological order: and negatives are fine.
- Same heap, different relaxation —
max(d, w)for bottleneck,minfor widest, multiply for probabilities. - Path — store
parent[v]when you relax, then walk back.
- Dijkstra: greedy + min-heap, , but only correct when every weight is non-negative.
- Bellman-Ford: relax every edge times, ; a
V-th round that still improves something means a negative cycle. - Floyd-Warshall: triple loop with
koutermost, , computes every pair’s shortest distance in one pass. - Default to Dijkstra; drop to Bellman-Ford for negative edges or cycle detection; reach for Floyd-Warshall only when you need all-pairs distances on a graph small enough for .
With traversal, components, and weighted shortest paths covered, you have the core toolkit for nearly every graph question that shows up in interviews and competitive programming alike.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading