🔮 Floyd-Warshall Fundamentals
🎯 What is the Floyd-Warshall Algorithm?
Floyd-Warshall is a classic dynamic programming algorithm that finds the shortest paths between all pairs of vertices in a weighted graph. It works for both directed and undirected graphs, handles negative edge weights (but not negative cycles), and is remarkably simple to implement — just three nested loops!
Deep Explanation
Core Insight: The algorithm considers each vertex as a potential intermediate point on the shortest path between any two vertices. For each pair (i, j), we ask: "Is it better to go from i to j directly, or via vertex k?" By iterating k from 0 to V-1, we build up optimal paths using more and more intermediate vertices.
Key Intuition: At iteration k, dist[i][j] represents the shortest path from i to j using only vertices {0,1,...,k-1} as intermediates. When we incorporate vertex k, we update: dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]).
def floyd_warshall(graph):
"""
graph: 2D list where graph[i][j] = weight from i to j
Use float('inf') for no direct edge
"""
V = len(graph)
dist = [row[:] for row in graph] # Deep copy
for k in range(V): # Intermediate vertex
for i in range(V): # Source vertex
for j in range(V): # Destination vertex
if dist[i][k] + dist[k][j] < dist[i][j]:
dist[i][j] = dist[i][k] + dist[k][j]
return dist
# Example usage
INF = float('inf')
graph = [
[0, 3, INF, 5],
[2, 0, INF, 4],
[INF, 1, 0, INF],
[INF, INF, 2, 0]
]
result = floyd_warshall(graph)
# result[i][j] = shortest distance from i to j📐 Algorithm Walkthrough
🎯 Step-by-Step Example
Consider a graph with 4 vertices (0,1,2,3). Initial distances matrix:
- k=0: Check paths through vertex 0. Update 2→1? 2→0(INF) skip. Update 3→2? 3→0(INF) skip.
- k=1: Check paths through vertex 1. Update 0→3? 0→1(3)+1→3(4)=7 > current 5 → no. Update 2→3? 2→1(1)+1→3(4)=5 → update.
- k=2: Check paths through vertex 2. Update 0→1? 0→2(INF) skip. Update 3→1? 3→2(2)+2→1(1)=3 → update.
- k=3: Check paths through vertex 3. Various updates may occur.
The Three Nested Loops
The order of loops is crucial: k (intermediate) must be the outermost loop. This ensures that when we consider vertex k as an intermediate, we already have optimal paths using vertices 0..k-1 as intermediates. This builds up the solution incrementally.
⏱️ Time & Space Complexity
🎯 Complexity Analysis
- Time Complexity: O(V³) — three nested loops, each iterating V times.
- Space Complexity: O(V²) — stores the distance matrix.
- Comparison: For dense graphs (E ≈ V²), Floyd-Warshall is optimal among all-pairs algorithms. For sparse graphs, running Dijkstra V times (O(V² log V + VE)) may be better.
- V = 1000: ~1 billion operations (may be slow). V = 500: ~125 million operations (acceptable).
📊 Dynamic Programming Formulation
🎯 Recurrence Relation
Let dist^{(k)}[i][j] = shortest path from i to j using vertices {0,1,...,k-1} as intermediates.
- Base Case (k=0): dist^{(0)}[i][j] = weight(i,j) (0 if i=j, INF if no direct edge)
- Recurrence: dist^{(k+1)}[i][j] = min(dist^{(k)}[i][j], dist^{(k)}[i][k] + dist^{(k)}[k][j])
- Final Answer: dist^{(V)}[i][j] = shortest path using all vertices as intermediates
Optimal Substructure
The recurrence exhibits optimal substructure: an optimal path either doesn't use vertex k as an intermediate, or consists of an optimal path from i to k followed by an optimal path from k to j.
💻 Multiple Implementations
# 1. In-place modification (most memory efficient)
def floyd_warshall_inplace(dist):
V = len(dist)
for k in range(V):
for i in range(V):
if dist[i][k] == float('inf'): continue
dik = dist[i][k]
for j in range(V):
if dist[k][j] != float('inf'):
new_dist = dik + dist[k][j]
if new_dist < dist[i][j]:
dist[i][j] = new_dist
return dist
# 2. With early termination for unreachable paths
def floyd_warshall_optimized(graph):
V = len(graph)
dist = [row[:] for row in graph]
for k in range(V):
for i in range(V):
if dist[i][k] == float('inf'):
continue
for j in range(V):
if dist[k][j] != float('inf'):
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
return dist
# 3. For undirected graphs (symmetric adjacency)
def floyd_warshall_undirected(graph):
V = len(graph)
dist = [row[:] for row in graph]
# Ensure symmetry (though input should already be symmetric)
for i in range(V):
for j in range(V):
if dist[i][j] != dist[j][i]:
dist[j][i] = dist[i][j] = min(dist[i][j], dist[j][i])
return floyd_warshall(dist)🛤️ Path Reconstruction
🎯 Reconstructing Shortest Paths
To reconstruct the actual shortest paths (not just distances), maintain a next matrix where next[i][j] stores the first vertex after i on the shortest path from i to j.
def floyd_warshall_with_path(graph):
V = len(graph)
dist = [row[:] for row in graph]
next_node = [[None if row[j] == float('inf') else j for j in range(V)] for row in graph]
for i in range(V):
next_node[i][i] = i
for k in range(V):
for i in range(V):
for j in range(V):
if dist[i][k] + dist[k][j] < dist[i][j]:
dist[i][j] = dist[i][k] + dist[k][j]
next_node[i][j] = next_node[i][k] # Path: i -> ... -> k -> ... -> j
def get_path(i, j):
if next_node[i][j] is None:
return None
path = [i]
while i != j:
i = next_node[i][j]
path.append(i)
return path
return dist, get_path⚠️ Negative Weights & Cycles
🎯 Handling Negative Edges
Floyd-Warshall handles negative edge weights correctly. However, if a negative cycle exists (a cycle with total negative weight), shortest paths are undefined (can be arbitrarily negative). The algorithm can detect negative cycles by checking if dist[i][i] < 0 after completion — indicating a negative cycle reachable from i.
def floyd_warshall_negative_cycle_detection(graph):
V = len(graph)
dist = [row[:] for row in graph]
for k in range(V):
for i in range(V):
for j in range(V):
if dist[i][k] + dist[k][j] < dist[i][j]:
dist[i][j] = dist[i][k] + dist[k][j]
# Check for negative cycles
for i in range(V):
if dist[i][i] < 0:
return None, True # Negative cycle detected
return dist, False📊 Step-by-Step Visualization
🎯 Graph with 4 vertices
Initial distance matrix:
- 0→1: 3, 0→3: 5
- 1→0: 2, 1→3: 4
- 2→1: 1
- 3→2: 2
After k=0: No changes (no paths through vertex 0 improve existing paths)
After k=1: 2→3 improves: 2→1(1)+1→3(4)=5 (was INF). 0→3: 0→1(3)+1→3(4)=7 >5 (no change)
After k=2: 3→1: 3→2(2)+2→1(1)=3 (was INF). 3→3: 3→2(2)+2→1(1)+1→3(4)=7 → negative? No.
After k=3: Check paths through 3: 0→2: 0→3(5)+3→2(2)=7 (was INF). 1→2: 1→3(4)+3→2(2)=6 (was INF).
✅ Correctness Proof & Invariants
🎯 Proof by Induction
Invariant: After the k-th iteration of the outer loop, dist[i][j] contains the shortest path from i to j using only vertices {0,1,...,k-1} as intermediate vertices.
Base case (k=0): No intermediate vertices allowed, so dist[i][j] = direct edge weight (or INF).
Inductive step: When considering vertex k, the shortest path from i to j either includes k as an intermediate or it doesn't. If it includes k, then the optimal path is optimal(i→k) + optimal(k→j), which we have from previous iterations. The min of these two options gives the optimal path using vertices up to k.
📈 Comparison with Other AP Algorithms
- vs V × Dijkstra: O(V² log V + VE) with binary heap, O(V³) with array. Better for sparse graphs.
- vs V × Bellman-Ford: O(V²E) — much slower than Floyd-Warshall for dense graphs.
- vs Johnson's Algorithm: O(VE log V) — reweights edges to make all weights non-negative, then runs Dijkstra V times. Best for sparse graphs with negative edges.
- When to use Floyd-Warshall: Dense graphs, small V (≤500), simple implementation needed, or transitive closure problems.
⚡ Optimizations & Variants
- Min-Plus Matrix Multiplication: Floyd-Warshall is essentially repeated min-plus multiplication.
- Blocked/Parallel Floyd-Warshall: Partition matrix into blocks for better cache locality and parallel execution.
- Space Optimization: For path reconstruction, store only next pointers (O(V²) space).
- Transitive Closure (Unweighted): Use Boolean OR/AND instead of min/plus — O(V³) but can be optimized with bitsets to O(V³/wordsize).
🌍 Real-World Applications
- Network Routing: Computing all-pairs shortest paths for routing tables in small networks.
- Graph Analytics: Computing graph diameter, eccentricity, closeness centrality.
- Transitive Closure: Determining reachability in directed graphs (e.g., dependency resolution).
- Minimum Cost Path in Grids: With obstacles and varying costs.
- Database Query Optimization: Join order optimization in some database systems.
- Social Network Analysis: Computing degrees of separation between all users.
🔗 Transitive Closure
🎯 Reachability in Graphs
Transitive closure determines if there exists a path from vertex i to vertex j. Floyd-Warshall can be adapted using Boolean logic: reach[i][j] = reach[i][j] OR (reach[i][k] AND reach[k][j]). This is useful for reachability analysis, parsing grammars (CYK algorithm), and dependency graphs.
def transitive_closure(graph):
"""
graph: adjacency matrix (True = edge exists)
Returns reachability matrix
"""
V = len(graph)
reach = [row[:] for row in graph]
for i in range(V):
reach[i][i] = True
for k in range(V):
for i in range(V):
if reach[i][k]:
for j in range(V):
if reach[k][j]:
reach[i][j] = True
return reach
# With bitset optimization (faster)
def transitive_closure_bitset(adj):
V = len(adj)
reach = [row[:] for row in adj]
for i in range(V):
reach[i][i] = 1
for k in range(V):
for i in range(V):
if reach[i][k]:
# OR the entire row at once
for j in range(V):
reach[i][j] |= reach[k][j]
return reach💪 Practice & Problem Patterns
🎯 Classic Floyd-Warshall Problems
- Find the City With Smallest Number of Neighbors (LeetCode 1334): Use Floyd-Warshall to compute all-pairs distances, then count reachable cities within threshold.
- Detect Negative Cycles (GFG): Standard cycle detection after Floyd-Warshall.
- Shortest Path in a Grid with obstacles and weights: Convert grid to graph, run Floyd-Warshall.
- Course Schedule IV (LeetCode 1462): Transitive closure to determine prerequisites.
- Minimum Cost to Connect All Nodes: After computing all-pairs shortest paths, MST becomes easier.
- Cheapest Flights Within K Stops (variant): While usually Bellman-Ford, can be solved with Floyd-Warshall by storing hops information.
# LeetCode 1334 - Find the City With Smallest Number of Neighbors
def findTheCity(n, edges, distanceThreshold):
INF = float('inf')
dist = [[INF] * n for _ in range(n)]
for i in range(n):
dist[i][i] = 0
for u, v, w in edges:
dist[u][v] = w
dist[v][u] = w
# Floyd-Warshall
for k in range(n):
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]
# Count reachable cities within threshold
best_city = -1
min_count = n
for i in range(n):
count = sum(1 for j in range(n) if i != j and dist[i][j] <= distanceThreshold)
if count <= min_count:
min_count = count
best_city = i
return best_city