All-Pairs Shortest Paths
The single-source lesson found the shortest path from ONE source to everywhere. This lesson finds the shortest path between EVERY pair of vertices at once. By the end you will be able to implement and animate extendShortestPaths and the matrix-multiplication view (slowAllPairsShortestPaths and fasterAllPairsShortestPaths), floydWarshall with its D and Π matrices, transitiveClosure, and Johnson's reweighting algorithm for sparse graphs — and know exactly when negative-weight cycles break each method and how each method notices.
The all-pairs problem: W, D, Π
n warehouses. The single-source lesson answered "what is the cheapest route from warehouse 0 to everywhere else?". This lesson answers the harder question every dispatcher actually needs: "what is the cheapest route between every pair of warehouses?" — all n² answers, computed together, sharing work instead of running a single-source algorithm n separate times from scratch.The input is a weighted, directed graph G = (V, E) with a weight function w: E → ℝ (negative weights are allowed, as long as there is no negative-weight cycle reachable from a vertex that can also reach back to it). Vertices are numbered 0, 1, …, n−1, exactly like Dart list indices. The whole lesson represents the graph as an n × n weight matrix W = (wij):
wij = 0 if i = j, w(i,j) if (i,j) ∈ E, ∞ if (i,j) ∉ E and i ≠ j
Every algorithm in this lesson produces two matrices:
D = (dij), wheredij = δ(i,j), the true shortest-path weight fromitoj.Π = (πij), the predecessor matrix:πijis the vertex right beforejon a shortesti-to-jpath (or null, shown as "–", ifi = jor no path exists).
Given a row i of Π, printAllPairsShortestPath(Π, i, j) reconstructs the actual path by walking predecessors backward from j until it reaches i — the same idea as printing a path from a single-source π array in the graph-basics lesson, just reading from row i of Π. You will build and test exactly this reconstruction in the question bank.
i, column j answers a question about the pair (i, j). ∞ ("infinity") stands for "no such path", min means "keep the smaller", and δ(i,j) (delta) is the true shortest weight. A graph is sparse if it has few edges and dense if almost every pair of vertices has one. Θ(n³) ("theta of n cubed") means "grows like n × n × n" — three nested loops of n steps each.A tiny example first. Three depots with one-way roads: 0→1 costs 2, 0→2 costs 7, 1→2 costs 3, 2→0 costs 4, 2→1 costs 1. The three grids below are computed live by this page's own Floyd-Warshall code (later sections animate how):
Read D, row 0, column 2: the cheapest cost from depot 0 to depot 2 is 5, not the direct 7, because 0→1→2 costs 2 + 3. Read Π, row 0, column 2: the value 1 means "the last stop before 2 on that cheapest route is depot 1".
δ(i,j) for any pair the cycle can reach and be reached from (you could loop the cycle forever, making the "shortest" path weight −∞). Every algorithm below either assumes no negative cycle exists (extendShortestPaths, transitiveClosure), or actively detects one and reports it (the Floyd-Warshall diagonal check, Johnson's Bellman-Ford phase) — each example section below includes a dedicated negative-cycle demonstration so you see exactly how.n² numbers, so no method can beat Ω(n²). The obvious baseline — run a one-source algorithm from every vertex — costs n × O(VE) = O(V²E) with Bellman-Ford (Θ(V⁴) on a dense graph) or n × Dijkstra when all weights are nonnegative. This lesson gets Θ(V³) even with negative weights (Floyd-Warshall) or beats that on sparse graphs (Johnson), by sharing work between the n sources.dij = δ(i,j)), Π = the memory of how (the vertex just before j), and negative-weight cycle = the only thing that can make "shortest" meaningless.The maths as code. The weight matrix W and the size of the answer (n² numbers, so Ω(n²)) written as Dart.
Shortest paths and matrix multiplication
lij(m) = the minimum weight of any path from i to j that uses at most m edges. With zero edges allowed, lii(0) = 0 and every other lij(0) = ∞ (you can't get anywhere in zero steps). Growing m by one means: either the best ≤(m-1)-edge path was already good enough, or the best path takes one more hop through some vertex k — try every k and keep the cheapest (the at-most-m-edges recurrence):lij(m) = min0≤k≤n−1 ( lik(m-1) + wkj )
Since every simple shortest path uses at most n-1 edges, δ(i,j) = lij(n-1) = lij(n) = lij(n+1) = … — going further never helps once you've reached n-1 edges.
The maths as code. The at-most-m-edges recurrence: L[m] is the "at most m edges" matrix, built by the recurrence, and it stops changing at m = n−1.
Side by side: matrix multiplication vs shortest paths
Swap two operations and the "nothing" values that go with them, and every line of the ordinary matrix product becomes a line of extendShortestPaths:
| Role | Ordinary product | Min-plus product (shortest paths) |
|---|---|---|
| combine one candidate | aik · bkj (× ) | lik + wkj (+ ) |
| collect all candidates | Σk (add up) | mink (keep the smallest) |
| "nothing here" value (adding it changes nothing) | 0, because x + 0 = x | ∞, because min(x, ∞) = x |
| "do nothing" value (combining with it changes nothing) | 1, because x · 1 = x | 0, because x + 0 = x |
| identity matrix | 1 on the diagonal, 0 elsewhere | 0 on the diagonal, ∞ elsewhere (this is L(0)) |
| the m-th power of the matrix | Am | L(m) = cheapest routes using at most m edges |
Small example first. For the tiny 3-depot graph, cell (0,2): the ordinary product adds 0·7 + 2·3 + 7·0 = 6 (with ∞ written as 0), the min-plus product takes min(0+7, 2+3, 7+0) = 5. Same three candidates k = 0, 1, 2; different operations. Watch both products fill in together:
Notice the right-hand grid of Example 1 is exactly L(2) = W ⊙ W: every entry answers "cheapest route using at most 2 edges".
a + min(b, c) = min(a+b, a+c)). The big payoff is associativity, which is what lets us square the matrix below. Mathematicians call (min, +) the tropical semiring; the (OR, AND) pair used for transitive closure is the Boolean semiring.The maths as code. The ordinary matrix product next to the min-plus product, the identity matrix L(0), associativity and distributivity (a + min(b, c) = min(a+b, a+c)) as numeric checks.
extendShortestPaths(L, W)
This recurrence is exactly the recurrence for ordinary matrix multiplication, with min replacing + and + replacing × (and ∞ replacing the "0" identity, since min(x, ∞) = x just like x + 0 = x). extendShortestPaths computes one such "product" L' = L ⊙ W, turning ≤(m-1)-edge estimates L into ≤m-edge estimates L':
Three nested loops over i, j, k, each doing O(1) work — exactly like the ordinary matrix product — so extendShortestPaths costs Θ(n³).
Small example first. With the tiny 3-depot graph and L = W: l'02 = min(l00+w02, l01+w12, l02+w22) = min(0+7, 2+3, 7+0) = 5 — the two-edge route 0→1→2 (cost 5) beats the one-edge route (cost 7). The animations show every one of these trials:
l'ij finishes trying vertex k, the running value of l'ij equals mink'≤k(lik' + wk'j) — the best 1-more-edge path found among the first candidate intermediate vertices. After the last k, that minimum has been taken over all vertices, which is exactly the recurrence above.999999 + (−4) looks like a real route of cost 999995 and quietly beats "no route" — always test for ∞ before adding (the Dart code below does). (2) Writing the result into L itself while still reading it: entry l'ij would then use already-updated neighbours and mix different edge budgets, so keep L' a separate matrix.In Dart every loop is for (int i = 0; i < n; i++) and double.infinity plays ∞.
Input size → what is feasible: n ≤ 100 → one call is n³ = 106 steps (instant); n = 1000 → 109 steps per call (seconds — roughly 1 to 10 s depending on the machine and implementation), and the all-pairs drivers need several calls, so switch to Floyd-Warshall.
slowAllPairsShortestPaths(W)
Chain extendShortestPaths starting from L(1) = W (paths using at most 1 edge are just direct edges), calling it n-2 more times to reach L(n-1) = δ:
n-2 calls, each Θ(n³), give Θ(n⁴) total — correct, but wasteful.
Small example first. The tiny 3-depot graph has n = 3, so the loop for m from 2 to n−1 runs exactly once (m = 2): L(2) = L(1) ⊙ W is already δ, and the extra check finds a diagonal of zeros.
L(n-1) do ONE more extend round (a negative simple cycle can use all n edges, so n-1 edges can miss it); then any lii < 0 means a negative-weight cycle passes through vertex i, and every δ(i,j) the cycle can reach and return from is really −∞, not the finite number shown.n−1 edges. That is why L(n-1) = L(n) = …. With a negative cycle the sequence never stops improving, which is exactly what the extra round detects.Input size → what is feasible: n ≤ 30 → (n−2)·n³ = 28·27,000 ≈ 7.6·105 steps is fine; n = 100 → 98·106 ≈ 108 is painful; n = 500 → 6·1010 is hopeless, use faster or Floyd-Warshall.
Repeated squaring → fasterAllPairsShortestPaths(W)
The extendShortestPaths "product" is associative (the question bank asks you to prove it), so instead of always multiplying by W one edge at a time, you can square: compute L(2m) = L(m) ⊙ L(m), doubling how many edges are covered with every call. Starting from m=1, doubling reaches m ≥ n-1 after only ⌈lg(n-1)⌉ calls:
x16 you do not multiply x sixteen times — you square four times (x → x² → x⁴ → x⁸ → x¹⁶). The same trick works on our "route budget": knowing the cheapest routes of at most m edges, combine the table with itself — "first half of a route, then second half" — and you know the cheapest routes of at most 2m edges.Red = SLOW's one-step-at-a-time calls; green = FASTER's doubling calls (grey m = 1 is L(1) = W, given for free).
Small example first. For n = 5 the loop needs m to reach n−1 = 4: m = 1 → 2 → 4, so only 2 extend calls (SLOW would make 3). For n = 9 it is 3 calls instead of 7.
Θ(lg n) calls to a Θ(n³) routine give Θ(n³ lg n) — asymptotically better than slowAllPairsShortestPaths's Θ(n⁴) for large n. (Overshooting past exactly n-1 edges is harmless: δ stops changing once you reach n-1 edges.)
L with L, not with W: L ⊙ W only adds one edge of budget, which is SLOW again. (2) The loop condition is m < n−1, and m may overshoot n−1 (n = 6 ends at m = 8): that is harmless because δ does not change once the budget reaches n−1.1, 2, 4, …, 2t and the loop stops at the first 2t ≥ n−1, so t = ⌈lg(n−1)⌉ calls (see the ladder above and the cost table in the last section). Each call is Θ(n³): total Θ(n³ lg n).x16 with 4 squarings instead of 15 multiplications) — here applied to the "min-plus" matrix product instead of ordinary multiplication. It only works because that product is associative; a non-associative "product" could not be doubled this way. Cost: Θ(n³ lg n).The variable m counts edges (it is not an index), so while (m < n − 1) is exactly the loop condition above.
Input size → what is feasible: n ≤ 200 → ⌈lg 199⌉ = 8 calls × 8·106 = 6.4·107 steps is fine; n = 1000 → 10 calls × 109 = 1010 is too slow, Floyd-Warshall needs 109.
The maths as code. Θ(n³ lg n) versus Θ(n⁴) by counting calls: SLOW makes n−2, FASTER makes ⌈lg(n−1)⌉, each call costs n³.
The Floyd-Warshall algorithm
dij(k) = the shortest i-to-j path weight using only vertices {0, …, k−1} as intermediate stops (the endpoints i, j don't count as "intermediate"). With k=0 no intermediates are allowed, so dij(0) = wij (direct edge or ∞). Allowing one more vertex, k, asks one question: does routing through it ever help? (the stops recurrence):dij(k+1) = min( dij(k), dik(k) + dkj(k) )
and the matching predecessor update: keep the old predecessor if the direct route (without k) is still at least as good; otherwise the new predecessor is whatever k's own predecessor was on the k-to-j leg:
πij(k+1) = πij(k) if dij(k) ≤ dik(k) + dkj(k), else πkj(k)
floydWarshall(W)
Three nested loops (k, then i, then j), O(1) work each: Θ(n³) time. Only the current and previous D/Π matrices are ever needed at once, and you can even drop the copies and update a single n×n matrix in place — Θ(n²) space, the tightest of every algorithm in this lesson, and in practice among the fastest thanks to its tiny constant factor and simple loop structure.
Small example first. Triangle 0→1 (cost 2), 1→2 (cost 3), 0→2 (cost 7). With no stops d02 = 7. When vertex 1 becomes allowed (round k = 1), the test asks "is 2 + 3 = 5 less than 7?" — yes, so d02 becomes 5. Vertices 0 and 2 as stops change nothing. That single question, asked for every cell and every k, is the whole algorithm.
k begins, dij already equals the shortest-path weight from i to j using only intermediate vertices from {0, …, k−1}, for every i, j. The round restores the invariant for {0, …, k} by asking exactly one question per cell: "does vertex k help?" After the last round, intermediates are allowed to be any vertex, so D = δ.D. In a graph with no negative-weight cycle, every shortest path from a vertex to itself has weight exactly 0 (the empty path). If any dii < 0, that path loops through a negative-weight cycle back to i — the graph has one, and none of the reported distances that cycle can reach are trustworthy.In Dart a null replaces "no predecessor" in Π, and vertex numbers are the 0-based indices themselves.
Input size → what is feasible: n ≤ 400 → n³ = 6.4·107 steps, well under a second; n = 1000 → 109 steps (seconds — roughly 1 to 10 s depending on the machine and implementation): use Johnson if the graph is sparse; n = 104 → 1012 steps and 108 matrix cells, out of reach.
The maths as code. The stops recurrence as code (fwRounds), and the loop invariant "D(k) uses only stops 0…k−1" checked against a brute-force path search.
The maths as code. The Θ(n³) bound: the innermost body runs exactly n³ times whatever the edges are (n = 10, 100, 1000).
The maths as code. Why one Θ(n²) matrix suffices: row k and column k cannot change during round k, because dkk = 0.
Building the predecessor matrix Π inside floydWarshall
πkj. Everything earlier in the itinerary can be recovered by asking Π again ("what was the stop before that one?").Keep a matrix Π(k) next to each D(k), using the two update rules above. The pseudocode below is that idea written out:
Small example first. Tiny 3-depot graph: initially π02(0) = 0 (a direct edge 0→2 exists, so the vertex before 2 is 0) and d02 = 7. At k = 1: through 1 costs d01 + d12 = 2 + 3 = 5 < 7, so d02 := 5 and π02 := π12 = 1 ("the vertex before 2 on the 1→2 leg is 1"). Reading Π backward: 2 ← 1 ← 0, the route 0→1→2.
Each animation follows one chosen pair (i, j) through all n rounds while showing the whole D and Π matrices, so you can see the rule fire for every cell and, at every k, read the route straight out of row i of Π:
Cost: two O(1) assignments per cell instead of one, so still Θ(n³) time and Θ(n²) space. The Dart function below is the single-cell rule; the full floydWarshall panel earlier in this section applies it to every cell.
πij := πik. That is the vertex before k on the i→k leg, not the vertex before j. The new route ends "…→k→…→j", so the last hop into j belongs to the k→j leg: use πkj. A second subtle point: the test is "≤" (ties keep the old route and its old predecessor), so equal-cost alternatives never make Π flip-flop.Π(k) describes a tree rooted at i whose tree paths are shortest paths that use only intermediate vertices from {0, …, k−1}. When dij improves through k, the new path is (tree path to k) + (tree path from k to j); its last edge is the last edge of the k→j tree path, which is πkj. Everything before that edge is still described by the unchanged part of row i. With a negative-weight cycle the invariant fails and Π can contain a cycle — the trace in the next section shows what that does to path printing.Input size → what is feasible: same size class as Floyd-Warshall (n ≤ 400): Π adds n² = 1.6·105 entries and one more assignment per cell.
The maths as code. Row i of Π is a shortest-path tree: following π from j back to i adds up to exactly dij.
printAllPairsShortestPath(Π, i, j)
The procedure is the path-printing idea from the graph basics lesson: it reads row i of Π instead of a single-source π array.
Small example first. With Π from the tiny 3-depot graph (π02 = 1, π01 = 0): print(Π,0,2) sees 0 ≠ 2 and π02 = 1 ≠ null, so it calls print(Π,0,1); that call sees π01 = 0 and calls print(Π,0,0), which prints 0; back in the middle call it prints 1; back in the outer call it prints 2. Output: 0 1 2.
Running time: each recursive call moves to a different vertex of one shortest path, so a valid Π gives at most n calls — O(n), or exactly Θ(number of vertices on the path) — no matter how large the graph is. The Dart code has a recursive version and an iterative one (used in the question bank).
j before the recursive call prints the route backwards. (2) Using the wrong row: the row is the source i (πij), not column i. (3) Feeding it a Π built from a graph with a negative-weight cycle: row i can contain a cycle and the recursion never reaches the base case (try the custom player with a negative cycle; our trace stops after n calls, real code would overflow the stack).(πij, j) of row i form a shortest-path tree rooted at i. Walking from j toward the root along parent pointers therefore reaches i in at most n−1 steps, and each step's edge lies on a shortest i-to-j path (subpaths of shortest paths are shortest). Printing after the recursive call reverses "j → root" into "root → j".Input size → what is feasible: n ≤ 1000 with up to 104 queries → at most 107 steps, because each query costs one step per vertex of its route.
The maths as code. The O(n) bound: one recursive call per vertex of the route.
Transitive closure
i to j at all?" That's the transitive closure G* = (V, E*), where (i,j) ∈ E* exactly when some path exists from i to j in G. You could run Floyd-Warshall with every edge weight set to 1 and check dij < ∞ — but a direct boolean version, replacing min/+ with OR/AND, avoids arithmetic entirely and can be implemented with fast bitwise operations on real hardware.Let tij(k) = 1 if a path from i to j exists using only intermediate vertices in {0,…,k−1}, else 0:
tij(k+1) = tij(k) ∨ ( tik(k) ∧ tkj(k) )
transitiveClosure(n, edges)
Small example first. Edges 0→1 and 1→2 only. Initially t02 = 0 (no direct edge). When vertex 1 becomes an allowed stop (k = 1): t01 = 1 and t12 = 1, so t02 = 0 ∨ (1 ∧ 1) = 1 — 2 is reachable from 0 through 1.
Same three nested loops as Floyd-Warshall: Θ(n³) time, Θ(n²) space.
tii(0) = 1: every vertex reaches itself by the empty path). Forgetting that breaks the recurrence for paths whose first or last stop is k itself. Also remember the matrix says "a path exists", not "an edge exists": the closure of a chain 0→1→2 has an extra 1 at (0,2).dij < ∞" — same Θ(n³) but slower arithmetic. (2) Pack each row into machine words and replace the inner j-loop by "if t[i][k] then row[i] |= row[k]": Θ(n³/w) word operations for word size w — the bitmask version in the question bank does exactly this.min/+ in this pattern can be swapped to a different pair of operations (here ∨/∧) as long as they form a "closed semiring".In Dart edges are pairs of 0-based indices, and true/false replace 1/0.
Input size → what is feasible: n ≤ 400 → n³ = 6.4·107 boolean steps is fine; n ≤ 62 packed into 64-bit words → only n² = 3,844 word operations; n = 104 → even n³/64 ≈ 1.6·1010 is heavy, condense strongly connected components first.
The maths as code. The two alternatives from the text: Floyd-Warshall with every weight 1 (read "d < ∞"), and bit-packed rows with the Θ(n³/w) word count.
Johnson's algorithm for sparse graphs
The reweighting fact: pick any function h: V → ℝ and define ŵ(u,v) = w(u,v) + h(u) − h(v). For any path p = ⟨v0, v1, …, vk⟩, the sum ŵ(p) = w(p) + h(v0) − h(vk) — every interior h term telescopes away, leaving only the two endpoints. Since every i-to-j path picks up the same +h(i) − h(j) adjustment, whichever path was shortest under w is still shortest under ŵ — reweighting cannot change the answer, only the numbers along the way. (For a cycle, v0 = vk, so ŵ(cycle) = w(cycle) exactly — reweighting cannot hide or create a negative cycle either.)
To get nonnegative ŵ, add a new vertex s with a 0-weight edge to every real vertex, forming G'. Run Bellman-Ford once from s: if it reports a negative-weight cycle, G has one too, and Johnson's stops right there. Otherwise set h(v) = δ(s,v). The triangle inequality for shortest paths, δ(s,v) ≤ δ(s,u) + w(u,v), rearranges to exactly ŵ(u,v) = w(u,v) + h(u) − h(v) ≥ 0 for every edge.
Reweighting on the graph: the h values and why every ŵ ≥ 0
h(v) and adjust every road's toll by the drop in altitude: ŵ = w + h(u) − h(v) (driving downhill makes the toll bigger, uphill smaller). Now choose "altitude = the cheapest cost to reach that city from a depot s". Arriving in v by any road can never be cheaper than the best known way to v — so no road ends up with a negative adjusted toll. And a whole trip from i to j always picks up the same adjustment h(i) − h(j), because the altitudes of all the cities in between cancel.Small example first. In the 5-vertex graph used below, the edge 1→3 has weight −4 (negative!). Bellman-Ford from s gives h(1) = 0 and h(3) = −4, so ŵ(1,3) = −4 + 0 − (−4) = 0. The edge 0→3 (weight 3) has h(0) = 0 and h(3) = −4: ŵ(0,3) = 3 + 0 − (−4) = 7. The players below reweight every edge, one at a time, and show which inequality guarantees the result is never negative.
h(v) = δ(s,v) guarantees ŵ ≥ 0; (2) using the wrong sign: it is + h(u) − h(v) (tail minus head) on the edge and + h(v) − h(u) when undoing the shift on a result; (3) forgetting that s only exists to define h: s and its 0-edges are dropped before Dijkstra runs.δ(s,v) ≤ δ(s,u) + w(u,v), i.e. h(v) ≤ h(u) + w(u,v), i.e. ŵ(u,v) = w(u,v) + h(u) − h(v) ≥ 0. Edges with ŵ = 0 are the "tight" ones that lie on a cheapest route from s.h(i) − h(j), so the winning route is unchanged and Dijkstra becomes legal.The maths as code. The reweighting fact as code: path weights shift by h(start) − h(end) (interior terms telescope), cycles do not change, h = δ(s, v) makes every ŵ ≥ 0, and the un-reweighting formula.
Input size → what is feasible: reweighting costs O(E): E = 106 edges is instant; the Bellman-Ford step costs O(VE), so V·E ≤ 108 (for example V = E = 104) is the practical limit.
johnson(n, edges)
One Bellman-Ford run costs O(VE). n Dijkstra runs on the reweighted graph cost O(V² lg V + VE) total with a Fibonacci heap (or O(VE lg V) with a binary heap). Either way, for a sparse graph (E = O(V)), this beats Floyd-Warshall's Θ(V³).
d = δ̂ + h(v) − h(u), which returns distances that are off by exactly the h difference; and not stopping when Bellman-Ford reports a negative cycle — with one present, h does not exist.ŵ are the same paths as under w, so Dijkstra on (G, ŵ) finds the right paths; the final un-reweighting d(u,v) = δ̂(u,v) + h(v) − h(u) just undoes the telescoping adjustment to recover the true weight δ(u,v) under the original w.O(V² lg V + VE) with Fibonacci heaps — for sparse graphs (E = O(V)) that is roughly V² lg V, beating Floyd-Warshall's V³.The new source s becomes index n (one past the last real vertex); every real vertex keeps its index.
Input size → what is feasible: V = 1000, E = 5000 (sparse) → about 6·107 heap steps, fine, while Floyd-Warshall needs 109; V = 200, E = 39,800 (dense) → Floyd-Warshall is simpler and faster.
Comparing the algorithms: running-time analysis
Small example first. With V = 10: one extend call is 10³ = 1,000 steps. SLOW makes 8 calls (8,000), FASTER ⌈lg 9⌉ = 4 calls (4,000), Floyd-Warshall 1,000. The cost table below computes these numbers live for any size and shows why the ranking flips between sparse and dense graphs:
Where each bound comes from (derivations, one line each):
| Algorithm | Counting argument | Result |
|---|---|---|
| extendShortestPaths | 3 nested loops (i, j, k), n iterations each, O(1) body | Θ(n³) |
| slowAllPairsShortestPaths | calls for m = 2 … n−1 → n−2 calls × Θ(n³) | Θ(n⁴) |
| fasterAllPairsShortestPaths | m doubles until ≥ n−1 → ⌈lg(n−1)⌉ calls × Θ(n³) | Θ(n³ lg n) |
| floydWarshall | loops k, i, j, n iterations each, O(1) body (no inner calls) | Θ(n³) |
| transitiveClosure | same three loops with ∨/∧ | Θ(n³) |
| johnson | Bellman-Ford O(VE) + V × Dijkstra O(V lg V + E) (Fibonacci heap) | O(V² lg V + VE) |
V·E·(lg V) (binary heap) is well below V³, i.e. roughly when E < V²/lg V. On graphs with E = Θ(V) the gap is a factor ~V / lg V; at E = Θ(V²) Johnson is about a factor lg V worse. Space: Floyd-Warshall and the two matrix methods need Θ(n²) for the answer matrix anyway; Johnson needs only O(V + E) beyond the output.The maths as code. The bounds V³ (Floyd-Warshall), V·E + V(V+E) lg V (Johnson, binary heap) and V² lg V + VE (Fibonacci heap) evaluated for the two sizes in the cost table.
Quiz
Interview questions
Cheat sheet
| Algorithm | Time (best / average / worst) | Space | Negative weights? | Use when… |
|---|---|---|---|---|
| extendShortestPaths (1 call) | Θ(n³) in all cases | Θ(n²) | Yes (no negative cycle) | One "min-plus product" step; building block for the two below |
| slowAllPairsShortestPaths | Θ(n⁴) in all cases | Θ(n²) with two matrices (Θ(n³) if every L(m) is kept) | Yes (no negative cycle) | Teaching the matrix-multiplication view; never in production |
| fasterAllPairsShortestPaths | Θ(n³ lg n) in all cases | Θ(n²) (two matrices, swapped) | Yes (no negative cycle) | Matrix-multiplication view with only ⌈lg(n−1)⌉ squarings |
| floydWarshall (with Π) | Θ(n³) in all cases | Θ(n²) | Yes; negative diagonal ⇒ negative cycle | Dense graphs, or any graph when Θ(n³) is affordable — simplest correct choice, smallest constant |
| printAllPairsShortestPath | Θ(k) for a path of k vertices; O(n) worst, Θ(1) when i = j or null | O(n) recursion depth | Needs a valid Π (no negative cycle) | Turning row i of Π into an actual route |
| transitiveClosure | Θ(n³) in all cases (Θ(n³/w) bit-packed) | Θ(n²) bits | N/A (reachability only) | "Can i reach j at all?" queries, ignoring weight |
| johnson | O(V² lg V + VE) Fibonacci heap; O(VE lg V) binary heap; early exit O(VE) when Bellman-Ford finds a negative cycle | O(V²) output + O(V+E) | Yes; the Bellman-Ford phase detects negative cycles up front | Sparse graphs (E = O(V)) — asymptotically beats Floyd-Warshall |
| Reweighting ŵ = w + h(u) − h(v) | O(E) after h is known | O(E) | Produces only ŵ ≥ 0 when h = δ(s,·) | Making Dijkstra legal on a graph with negative edges |
"Commonly reported" company tags in the question bank reflect publicly shared interview experiences and are indicative, not verified lists.