A weighted directed graph of V vertices, and the question is not one shortest path but the distance between every ordered pair — a full V×V table. Running a single-source algorithm from each vertex answers it, yet on a dense graph that repeats most of the work, and a negative edge weight rules out the fastest single-source choice outright.

Floyd-Warshall fills the whole table with one triple loop by recasting the problem as Dynamic Programming over a growing set of permitted waypoints. The sub-problem is “the shortest path from i to j that may route only through intermediate vertices drawn from {0..k}.” Beginning with direct edges alone and admitting one more permitted intermediate per stage, the last stage leaves every entry at its unrestricted shortest distance. Each stage k poses a single question at every pair: keep dist[i][j], or improve it by going i → k → j. The distances stay meaningful only while no negative cycle exists — a negative cycle has no finite shortest path, and the algorithm reports it instead of returning a number.

Core shape: weighted graph → dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]) with k admitted one vertex at a time → all-pairs distances in Θ(V³) time and Θ(V²) space.

The decisive step is a single relaxation sweeping the whole distance matrix for one admitted intermediate vertex.

Visualization pending

Planned StepTrace: a matrix card showing a V×V distance table updated across each intermediate vertex k, with dist[i][j] relaxed by dist[i][k] + dist[k][j]. No matching renderer exists in engine.js yet.

Why one intermediate at a time works

The state is a single V×V matrix dist, initialized so dist[i][j] is the direct edge weight, dist[i][i] is 0, and every other entry is . Stage k runs one relaxation over the whole matrix:

dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])

The invariant the stages maintain: once stage k finishes, dist[i][j] is the shortest ij distance using intermediate vertices drawn only from {0..k}. Each pair has exactly two ways to satisfy the next stage. Either the best path through {0..k} avoids k, and the old value already holds it; or it passes through k exactly once, splitting into an ik leg and a kj leg that each use only earlier intermediates — precisely dist[i][k] + dist[k][j] carried over from the previous stage. Taking the smaller of the two extends the invariant to {0..k}.

That decomposition is why k is the outermost loop. It reads dist[i][k] and dist[k][j] as the previous stage left them, so the entire matrix has to finish updating for one k before the next begins. Running i or j outside k mixes cells from two different stages into one relaxation, and the recurrence consumes half-finished data.

The same decomposition makes the in-place update safe on one matrix rather than a fresh copy per stage. During stage k neither dist[i][k] nor dist[k][j] can improve — a shortest path through k never uses k as an intermediate of its own legs — so reading and writing the same array yields the values a separate previous copy would have held. That is what collapses the natural Θ(V³) space of the layered DP down to Θ(V²).

A four-vertex run shows the layering. is shown as .:

Vertices 0..3, directed edges (weight):
  0→1 (3)  0→3 (7)  1→0 (8)  1→2 (2)  2→0 (5)  2→3 (1)  3→0 (2)
 
dist after init:            final all-pairs distances:
      0   1   2   3               0   1   2   3
  0 [ 0   3   .   7 ]         0 [ 0   3   5   6 ]
  1 [ 8   0   2   . ]         1 [ 5   0   2   3 ]
  2 [ 5   .   0   1 ]         2 [ 3   6   0   1 ]
  3 [ 2   .   .   0 ]         3 [ 2   5   7   0 ]

dist[0][3] holds the direct edge 7 until vertex 2 becomes admissible at stage k = 2, where 0→2→3 costs 5 + 1 = 6 and wins. dist[1][3] first drops to 15 through vertex 0 at k = 0, then to 3 at k = 2 via 1→2→3. No diagonal entry ends negative, so the graph carries no negative cycle.

Reference drawer

Complexity

CaseTimeSpaceCause
Every inputΘ(V³)Θ(V²)Three fixed-length loops run relaxations with no early exit; cost depends on V alone, never on edge count or weights. The matrix is Θ(V²) and doubles as the output, so the in-place update adds only O(1) auxiliary.

Best, average, and worst coincide because nothing in the data shortens the sweep — a complete graph and an edgeless one both take the same steps. The single honest bound is Θ(V³). Path reconstruction adds a second Θ(V²) next matrix; the true auxiliary cost beyond the output matrix stays O(1) without it. The naive layered DP that keeps one matrix per stage would need Θ(V³) space, which the in-place argument above removes.

When the reported distances are wrong

A negative edge is fine on its own — a stage relaxes through it and the invariant still holds. A negative cycle is not: looping it lowers the total without bound, so the true shortest distance is −∞, while the sweep stops at whatever finite value its relaxations happened to reach. The signal lives on the diagonal. Any dist[i][i] < 0 means vertex i sits on a negative cycle, since the only way back to i with negative weight is around one. Every off-diagonal dist[u][v] whose route can pass through such an i — both dist[u][i] and dist[i][v] finite — is equally invalid and belongs marked −∞ rather than read as a number.

Reordering the loops so i or j is outermost still compiles, runs, and terminates, but it relaxes pairs against cells from a stage that has not finished. The matrix comes back full of finite numbers that are simply wrong wherever a shortest path needed an intermediate whose row or column was consulted before that stage completed. Because nothing crashes, the defect hides until a specific graph exposes it.

Overflow is the other silent corruptor. With int.MaxValue as , the unconditional dist[i][k] + dist[k][j] wraps to a large negative number whenever both operands are the sentinel, and that phantom shortcut then propagates through the rest of the sweep. Skipping the relaxation when either operand is , or using a sentinel such as long.MaxValue / 4 that tolerates one addition, closes it.

Questions

References