Context:FIT2004_MOC · all-pair shortest distance on an adjacency matrix, negative edges allowed — three nested loops that beat running Dijkstra or Bellman-Ford from every vertex
Parent Framework:Graph
Quick Revision
🎯 Objective: for every intermediate vertex k in turn, ask “is i⇝k⇝j cheaper than i⇝j?” ➔ after all ∣V∣ values of k, every entry is the true shortest distance.
⚡ Key Constraint:Θ(V3) time and Θ(V2) space unconditionally — it never looks at E, so it is the wrong tool on a sparse graph and the right one on a dense one.
The object ➔ given G=(V,E), the transitive closure is G′=(V,E′): the same vertices, plus an edge ⟨u,v⟩ whenever a pathu⇝v exists in G.
The rule is transitivity ➔ A→B and B→C therefore A→C; on the boolean adjacency matrix that reads m[i][j]andm[j][k]⇒m[i][k].
One line of code ➔ m[i][j] = m[i][j] or (m[i][k] and m[k][j]) inside for k / for i / for j.
Answers the reachability question in O(1) afterwards ➔ the closure is a lookup table; building it once beats V separate traversals only when the graph is dense (Θ(V3) vs Θ(V(V+E))).
Cost ➔ time Θ(V3), space Θ(V2) for the matrix.
2. Floyd-Warshall — the Same Loop, With Distances
The upgrade ➔ replace the boolean semiring (or,and) with the arithmetic one (min,+): m[i][j] = min(m[i][j], m[i][k] + m[k][j]).
The transitivity becomes an inequality ➔ dist(A⇝B)+dist(B⇝C) is a cost of A⇝C; taking the min over every intermediate B makes it the shortest.
Doing it for every ⟨i,j⟩ gives all pairs ➔ the matrix is the output, so there is no source vertex and no per-source rerun.
Initialisation, in three moves ➔ ① diagonal m[u][u]=0(itself back to itself costs nothing) ② every edge ⟨u,v,w⟩ written as m[u][v]=w ③ ∞ for every non-edge.
Negative edges are fine ➔ nothing is finalised and nothing is compared for “closest”, so Dijkstra’s precondition never applies.
3. Why k Must Be the OUTER Loop (the DP layering — LO1)
The subproblem ➔ after the k-th outer iteration, m[i][j] is the shortest i⇝j walk whose intermediate vertices all come from {1,…,k}. The endpoints are unrestricted.
The recurrence ➔ dist(k)[i][j]=min(dist(k−1)[i][j],dist(k−1)[i][k]+dist(k−1)[k][j]) — either the new intermediate k helps, or it does not.
The lecture’s phrasing ➔ “as we increment, we find the minimum distance going through vertex k; thus we would have the minimum through every vertex, updating as needed.”
This is Dynamic Programming ➔ optimal substructure over a growing set of permitted intermediates, V3 subproblems at O(1) each ⟹ Θ(V3) falls straight out of the MEMO-sizing rule.
In-place is safe ➔ row k and column k cannot change during iteration k(using k as its own intermediate adds 0), so one V×V matrix suffices instead of ∣V∣+1 of them.
4. Negative-Cycle Detection — Read the Diagonal
The test ➔ after the run, anym[u][u]<0 means a negative cycle exists. The diagonal is “vertex u back to vertex u”, so a negative entry is literally a negative closed walk.
Stronger than Bellman-Ford’s ➔ Bellman-Ford only sees cycles reachable from its source; Floyd-Warshall considers every vertex as a start, so it finds any negative cycle in the graph.
What to report ➔ distances through a negative cycle are −∞, not numbers; state that no shortest distance exists for those pairs rather than handing over the matrix.
The −∞ post-process(applied P5) ➔ for every k with m[k][k]<0, set m[u][v]=−∞ for every pair with m[u][k]<∞ and m[k][v]<∞ — u reaches the cycle and the cycle reaches v ⟹ one more triple loop, O(V3), bound unchanged.
Contamination is wider than the diagonal ➔ one flagged k can send most of the matrix to −∞, so a single negative diagonal entry invalidates far more than one cell.
5. Reading the Path Out, Not Just the Distance (applied P4)
Distances alone are not paths ➔ keep a second V×V matrix, written only when the min takes the u→k→v branch ⟹ still Θ(V3) time and Θ(V2) space.
succ — the sheet’s preferred form ➔ succ[u][v]= the first vertex after u on a shortest u⇝v. Init succ[u][v]=v for every edge; on an update succ[u][v]=succ[u][k](the first step towards v is now the first step towards k). Rebuild by u = succ[u][v] until u=v ⟹ O(ℓ) with plain appends.
mid — divide and conquer ➔ on an update mid[u][v]=k; rebuild as path(u,mid)+ path(mid,v), dropping the duplicated middle vertex. O(ℓ)only if concatenation is O(1) ⟹ linked lists.
pred — the mirror of succ ➔ pred[u][v]=pred[k][v], walked backwards from v and reversed — the same move as Dijkstra’s v.previous, one dimension wider.
⚙️ Core Implementation
🔹 Warshall’s transitive closure
Code
# matrix[i][j] is True iff edge <i,j> exists; becomes True iff a PATH existsfor k in range(count_vertex): for i in range(count_vertex): for j in range(count_vertex): matrix[i][j] = matrix[i][j] or (matrix[i][k] and matrix[k][j])
💡 Common Mistake:Writing the loops as i, j, k ➔ the semantics live in the outer loop: k is the newly permitted intermediate, and i,j sweep every pair against it. Any other nesting computes paths of bounded length, not the closure, and one pass will not converge.
🔹 Floyd-Warshall all-pair shortest distance
Code
# init: matrix[u][u] = 0; matrix[u][v] = w and succ[u][v] = v for every edge; INF / None elsewherefor k in range(count_vertex): for i in range(count_vertex): for j in range(count_vertex): if matrix[i][k] + matrix[k][j] < matrix[i][j]: matrix[i][j] = matrix[i][k] + matrix[k][j] succ[i][j] = succ[i][k] # first hop towards j = first hop towards kfor u in range(count_vertex): if matrix[u][u] < 0: error("Graph contains a negative-weight cycle")
💡 Common Mistake:Forgetting to zero the diagonal ➔ leaving m[u][u]=∞ makes the negative-cycle test unreachable and lets ∞ leak into sums; the diagonal is both an initial value and the output certificate.
💡 Common Mistake:∞ as a large integer ➔ ∞+∞ overflows or, worse, becomes a plausible finite distance. Guard the addition or use a genuine ∞ sentinel.
🔹 Path rebuild from succ(applied P4)
Code
def get_path(succ, u, v): # assumes matrix[u][v] < INF path = [u] while u != v: u = succ[u][v] path.append(u) return path # O(l) for a path of l edges
💡 Common Mistake:Not initialising succ[u][v] = v per edge ➔ a pair never improved through an intermediate keeps None, and every rebuild that should end on a direct edge crashes.
🔹 −∞ post-process (applied P5 — in place of the error, when the matrix is wanted)
Code
for k in range(count_vertex): if matrix[k][k] < 0: # k lies on a negative cycle for u in range(count_vertex): for v in range(count_vertex): if matrix[u][k] < INF and matrix[k][v] < INF: # u reaches k, k reaches v matrix[u][v] = -INF
⚖️ Complexity
Adjacency matrix (mandatory — the algorithm indexes m[i][k] in O(1)).
Case
Time
Auxiliary space
Trigger
Best = Average = Worst
Θ(V3)
Θ(V2)
three fixed nested loops — no input-dependent branching, so there is no bad case, only a big V
Warshall’s closure
Θ(V3)
Θ(V2)
identical loop, boolean payload
With path reconstruction
Θ(V3)
Θ(V2)
a second matrix is the same order
With −∞ post-process
Θ(V3)
Θ(V2)
one more triple loop, only for flagged k
Derivation ➔ V×V×V iterations ×O(1) work =Θ(V3); init is Θ(V2) and the diagonal check Θ(V), both dominated.
The matrix is INPUT, the answer is OUTPUT ➔ if the graph arrives as a matrix, the Θ(V2) is input space; a list-to-matrix conversion makes it auxiliary (Algorithmic Complexity §6). Say which you are counting.
E never appears ➔ a sparse graph costs exactly as much as a complete one, which is the whole selection rule below.
When It Flips: the hinge is density, not weight sign. V Dijkstras cost O(EVlogV) — directly when w≥0, after Johnson's reweighting when some w<0 — and beat Θ(V3) exactly while ElogV⋘V2, i.e. on a sparse graph. Once E≈V2, the lecture's comparison Θ(V3)<O(V3logV)<O(V4) makes Floyd-Warshall the outright winner.
Initial matrix(diagonal 0 · edges written in · ∞ elsewhere)
A
B
C
D
A
0
∞
−2
∞
B
4
0
3
∞
C
∞
∞
0
2
D
∞
−1
∞
0
Manual Execution Trace
Only cells that actually change are listed; every other cell survives the round unchanged.
k(new intermediate)
Cell
Old
New
Arithmetic
A
[B][C]
3
2
B→A(4)+A→C(−2)
B
[D][A]
∞
3
D→B(−1)+B→A(4)
B
[D][C]
∞
1
D→B(−1)+B⇝C(2)
C
[A][D]
∞
0
A→C(−2)+C→D(2)
C
[B][D]
∞
4
B⇝C(2)+C→D(2)
D
[A][B]
∞
−1
A⇝D(0)+D→B(−1)
D
[C][A]
∞
5
C→D(2)+D⇝A(3)
D
[C][B]
∞
1
C→D(2)+D→B(−1)
Final Extracted Output
A
B
C
D
A
0
−1
−2
0
B
4
0
2
4
C
5
1
0
2
D
3
−1
1
0
Diagonal all 0 ⟹ no negative cycle; independently, the only cycle A→C→D→B→A weighs −2+2−1+4=3>0.
Every entry is finite ⟹ the transitive closure of this digraph is the complete graph on {A,B,C,D} — Warshall’s on the same input returns all-True.
Read the trap ➔ [D][C]=1 uses the already-updated[B][C]=2, not the original 3. In-place updating is correct here (§3), but you must carry the current matrix forward, not the initial one.
Read the layering ➔ the round in which a cell first becomes finite is the round that admits the last intermediate its route needs; [C][A]=5 waits for k=D because its route is C→D→B→A.
Applied Exercise — price the three all-pair routes (lecture comparison)
Problem: rank the three ways to get all-pair shortest distances on a dense graph.
Final Extracted Output:Θ(V3)<O(V3logV)<O(V4) ⟹ Floyd-Warshall wins on a dense graph. On a sparse graph the ranking inverts: V Dijkstras win for w≥0, and applied P7’s Johnson’s extends the same O(VElogV) to negative edges.
Applied Exercise — detect arbitrage (applied P6)
Problem:n currencies; r(a,b)= units of a spent to get 1 unit of b. Decide in O(n3) whether some chain of conversions makes a profit.
Model:A cycle costs:Take logs:Hence:a vertex per currency, edge a→b with rate r(a,b)r1r2⋯rk per unit returned⇒profit⟺r1r2⋯rk<1log(r1⋯rk)=∑ilogri,logr<0⟺r<1arbitrage⟺a negative cycle under w=logr⟺∃u:m[u][u]<0
Final Extracted Output: run plain Floyd-Warshall on log rates and read the diagonal, Θ(n3). Alternative ➔ keep the raw rates and swap (min,+) for (min,×); arbitrage iff a diagonal entry is <1.
The modelling move ➔ a product objective becomes a sum under log, so a shortest-path algorithm runs unmodified — transform the input, not the algorithm.
⚠️ Common Mistakes
💡 Nesting the loops i, j, k ➔ the outer loop is the set of permitted intermediates; any other order breaks the DP layering and needs repeated passes to converge.
💡 “Floyd-Warshall handles negative cycles” ➔ it detects them, on the diagonal. Distances through a negative cycle are −∞, not numbers.
💡 Using it on a sparse graph ➔ Θ(V3) is paid even when E=O(V); the selection rule is density, and quoting it without naming the density is an LO3 mark loss (Graph §3) · sparse with negatives:Johnson’s.
💡 Quoting Θ(V2) for the time ➔ that is the space. Three loops, Θ(V3) time, Θ(V2) space — state both, and say the matrix may be input rather than auxiliary.
💡 Confusing closure with connectivity ➔ the transitive closure is a directed reachability relation; u⇝v does not give v⇝u unless the graph is undirected.
🧠 Active Recall
Warshall's and Floyd-Warshall are the same three loops. State every difference, and what each buys.
Answer
Short answer: the payload only — boolean (or,and) for reachability, numeric (min,+) for distance; both Θ(V3) time and Θ(V2) space.
Why:Transitivity and the triangle inequality are the same statement in two semirings ➔ “a path i→k and a path k→j give a path i→j” becomes “the cost i→k plus the cost k→j is a candidate cost for i→j“. The extra work is initialisation ➔ booleans start False off-diagonal; distances start ∞ off-diagonal with the actual weights written in and the diagonal zeroed. Floyd-Warshall gains an output the closure has no analogue for ➔ the diagonal doubles as the negative-cycle certificate.
All pairs, negative edges, no negative cycle, ∣V∣=2000, ∣E∣=4000. Which algorithm, and defend the choice against Floyd-Warshall.
Hint: compute every candidate’s number before answering.
Answer
Short answer:Johnson’s — O(VElogV)≈2000⋅4000⋅11≈9×107 vs Floyd-Warshall’s Θ(V3)=8×109.
Why:Negative edges bar plain Dijkstra, not reweighted Dijkstra ➔ one Bellman-Ford from a super source (VE=8×106) gives potentials that make every weight ≥0, after which V Dijkstras are legal. V× Bellman-Ford is the wrong repetition ➔ O(V2E)=1.6×1013, because its own bound already carries a factor V. State the rule, not the instance ➔ all pairs: sparse ⟹ V Dijkstras (via Johnson’s if any w<0); dense ⟹ Floyd-Warshall.
After the run, m[C][C]=−4. What has happened, what do you report, and which cells can you still trust?
Answer
Short answer: a negative cycle through C exists; report that no shortest distance is defined, and trust no cell whose optimal route can reach the cycle.
Why:The diagonal is u⇝u ➔ a negative entry is a closed walk of negative weight, and repeating it drives the cost to −∞. Contamination spreads ➔ every pair ⟨i,j⟩ with i⇝C and C⇝j is also −∞, so a single flagged vertex can invalidate most of the matrix. This is the strength over Bellman-Ford ➔ Bellman-Ford only detects cycles reachable from its source; Floyd-Warshall treats every vertex as a source, so it finds a negative cycle anywhere in G. Repair ➔ the §4 post-process writes −∞ into exactly those cells in O(V3).