Transitive Closure asks a deceptively simple question about a directed graph: for every pair of vertices, can you get from one to the other — directly or by following a chain of edges? The answer is a full reachability table, and the trick to building it is one of the most reusable patterns in graph algorithms.
Problem. Given a directed graph as a boolean reachability matrix reach, where reach[i][j] = 1
means there is a direct edge from i to j, compute the transitive closure: set reach[i][j] = 1
whenever j is reachable from i through any path.
Example: edges 0 → 1 and 1 → 2. There is no direct edge 0 → 2, but you can chain them, so the
closure must add reach[0][2] = 1. Final answer: 0 reaches 1 and 2; 1 reaches 2.
The slow way first
The obvious idea: run a separate graph search (BFS or DFS) starting from every vertex, marking everything it can reach. That works and is O(V·(V + E)), which is fine for sparse graphs but fiddly to code and easy to get wrong with cycles.
The question to ask: is there a way to grow reachability without restarting a search from scratch each time? What if, instead of exploring paths, we let paths assemble themselves out of shorter known paths?
The idea: route through a middle vertex
Walk over every possible intermediate vertex k. For each pair (i, j), ask: can i reach k, and can k reach j? If both are already known, then i can reach j by passing through k. In one line:
reach[i][j] |= reach[i][k] && reach[k][j]
By looping k on the outside, every newly discovered link becomes available for the next k, so chains of any length get stitched together. This is Floyd–Warshall, specialized to booleans.
The key insight: k must be the outermost loop. That ordering guarantees that when we use reach[i][k], every path into k discovered so far is already recorded.
Walk through it
Step through the animation. The grid is the reachability matrix — rows are "from", columns are "to". The pointer k slides across, and for each k we highlight its row and column (the operands we read). When both reach[i][k] and reach[k][j] are 1, the target cell reach[i][j] flips on. Watch reach[0][2] light up when k = 1: that is 0 → 1 → 2 being discovered.
Pseudocode
n = number of vertices
for each intermediate vertex k in 0..n-1:
for each source i in 0..n-1:
for each target j in 0..n-1:
if reach[i][k] and reach[k][j]:
reach[i][j] = 1 # i can reach j via k
return reach # full transitive closureThe Python solution
def transitive_closure(reach):
n = len(reach)
for k in range(n):
for i in range(n):
for j in range(n):
reach[i][j] = reach[i][j] or (reach[i][k] and reach[k][j])
return reachreachis the boolean matrix:reach[i][j]is whetherican reachj. The diagonal is usually 1 (a vertex reaches itself).- The
kloop is outermost — that is the whole correctness argument. Eachklets us extend paths through that vertex. iandjscan every ordered pair of vertices.- The update
reach[i][j] = reach[i][j] or (reach[i][k] and reach[k][j])keeps any existing link and adds the new "viak" link. - After all
k, every cell answers the reachability question exactly.
Complexity
| Case | Time | Notes |
|---|---|---|
| BFS/DFS from every vertex | O(V·(V + E)) (moderate) | one search per source |
| Floyd-Warshall (this solution) | O(V³) (moderate) | triple loop over vertices |
O(V²) (moderate)The matrix approach is O(V³) time and O(V²) space (the matrix itself). For dense graphs it is often the simplest and fastest choice; its tight triple loop also vectorizes well with bitsets.
When this pattern shows up
Whenever a problem asks "is everything reachable / can i get from any node to any other / what is the
shortest path between all pairs," think Floyd-Warshall. The same triple loop computes all-pairs
shortest paths (swap the boolean OR/AND for min and +) — transitive closure is just its boolean cousin.
The loop order is not interchangeable. k must be the outer loop. If you put i or j outside, you
will use stale reachability values and miss longer chains. Memorize: intermediate vertex on the outside.
Practice
Edges are 0 → 1 and 1 → 2. While processing k = 1, which currently-zero cell becomes 1, and why?
1. Why must the k loop be the outermost of the three?
2. What does reach[i][j] |= reach[i][k] && reach[k][j] express?
3. What is the time complexity of the Floyd-Warshall closure on V vertices?
4. For edges 0 → 1 and 1 → 2, how many cells does the closure add beyond the direct edges?