Back to Graph Theory Series

Warshall's Algorithm (Transitive Closure)

October 4, 2026 Wasil Zafar 26 min read

For every ordered pair of vertices $(i,j)$, does any directed path lead from $i$ to $j$? Warshall's algorithm answers all $V^2$ reachability questions together in $O(V^3)$ time, using a compact dynamic program built entirely from Boolean OR and AND.

Contents

  1. Reachability & Closure
  2. The Boolean Bridge Rule
  3. DP Invariant & Correctness
  4. Worked Matrix Trace
  5. Diagonal Semantics
  6. Warshall vs Floyd–Warshall
  7. Implementation
  8. Bitset Acceleration
  9. Path Witnesses
  10. Complexity & Alternatives
  11. Applications
  12. Pitfalls & Checklist
  13. Exercises
  14. Historical Note

Reachability and Transitive Closure

A directed edge answers a one-step question: “can $i$ move directly to $j$?” Transitive closure answers the all-steps version: “can $i$ reach $j$ through a directed path of any permitted length?” The output is a Boolean matrix with one row per source and one column per destination.

$$R[i][j]=\begin{cases}1,&\text{if a directed path from }i\text{ to }j\text{ exists},\\0,&\text{otherwise.}\end{cases}$$

Adjacency

Records paths of exactly one edge: the relationships supplied in the input.

Reachability

Records whether at least one path exists, without caring about its length or weight.

Closure

Materializes every source–destination reachability answer for constant-time lookup.

Intuition: Add Transfer Hubs One at a Time

Imagine a flight map. First you know only direct flights. Then airport $k$ becomes an allowed transfer hub: any origin that can reach $k$ can now inherit every destination reachable from $k$. Warshall's algorithm repeats that row-sharing idea until every airport has been allowed as a transfer.

The Boolean Bridge Rule

When vertex $k$ becomes an allowed intermediate, a pair $(i,j)$ is reachable in one of two ways: it was already reachable without $k$, or a route can be split at $k$ into $i\leadsto k$ and $k\leadsto j$.

$$R[i][j]\leftarrow R[i][j]\lor\bigl(R[i][k]\land R[k][j]\bigr).$$
Warshall's Boolean bridge update through intermediate vertex k Vertex i reaches k and k reaches j, so a new dashed path certifies that i reaches j. The matrix update changes zero OR one AND one into one. Allow k as a transfer point i k j R[i][k] = 1 R[k][j] = 1 new witness: i → k → j R[i][j] ← 0 OR (1 AND 1) = 1
The update never removes reachability. It adds $(i,j)$ only when row $i$ can enter column $k$ and row $k$ already reaches column $j$.

This gives the familiar three nested loops. The $k$ loop must be outermost because it controls the set of legal intermediate vertices. The $i$ and $j$ loops may be interchanged without changing the invariant.

The DP Invariant and Correctness

Let $R^{(k)}[i][j]$ mean that a path from $i$ to $j$ exists whose internal vertices come only from $\{0,1,\ldots,k\}$. Endpoints $i$ and $j$ do not need to be in that set.

$$R^{(k)}[i][j]=R^{(k-1)}[i][j]\lor\left(R^{(k-1)}[i][k]\land R^{(k-1)}[k][j]\right).$$

Why this is complete: take any allowed path from $i$ to $j$. Either it does not use $k$ internally, so the first term already knows it, or it uses $k$. Split at one occurrence of $k$: both halves use only earlier allowed intermediates, so the second term detects them. Conversely, joining two real paths through $k$ produces a real walk from $i$ to $j$, hence valid reachability.

The $O(V^2)$ table can be updated in place. During phase $k$, the entries read from row/column $k$ already express reachability using intermediates through $k$; Boolean OR is monotone, and using those newly confirmed entries cannot invent a path outside the same permitted set.

Loop-Order Check

Putting $i$ or $j$ outermost destroys the “allowed intermediates” stages. The program may still work on some graphs, but the proof no longer applies and counterexamples exist. Keep for k outside both pair loops.

Worked Matrix Trace

Use edges $1\to2$, $2\to3$, $3\to1$, and $3\to4$. For this trace, the diagonal begins false: reachability requires at least one edge. That lets the algorithm discover $R[1][1]$, $R[2][2]$, and $R[3][3]$ as evidence of the directed cycle.

Initial and final strict transitive-closure matrices The graph contains a cycle among vertices one, two, and three plus an edge from three to four. The initial matrix has four direct edges. The final matrix shows that one, two, and three reach one another and four, while four reaches nothing. Graph 1 2 3 4 3-cycle plus one outgoing edge Initial adjacency direct edges only 1234 1234 0100 0010 1001 0000 all k Final strict closure new paths + cycle diagonal 1234 1234 1111 1111 1111 0000 k=1: 3 → 1 → 2 adds R[3][2]. k=2: 1 → 2 → 3 adds R[1][3]. k=3: the cycle closes rows 1–3 and reaches 4. Vertex 4 has no outgoing path, so its row stays zero. teal = direct edge · crimson = newly inferred · navy diagonal = nonempty cycle
Strict closure starts with a false diagonal. Warshall discovers the three navy diagonal cells because vertices 1, 2, and 3 lie on a directed cycle; vertex 4 does not.

Diagonal Semantics: Two Valid Questions

“Does a vertex reach itself?” has two conventions, and the initialization decides which one the matrix answers.

ConventionInitialize $R[i][i]$MeaningCycle test
Reflexive transitive closureTruePaths of length zero or moreDiagonal cannot distinguish cycles
Strict transitive closureFalse unless a self-loop existsPaths of one or more edges$R[i][i]=1$ iff $i$ lies on a directed cycle

Neither convention is universally “the” correct one. Dependency queries often want reflexivity (“a module depends on itself through zero steps” may be convenient); cycle analysis wants the strict form. Name the choice in the API instead of hiding it in initialization.

Warshall and Floyd–Warshall

The algorithms share the same intermediate-vertex dynamic program but operate over different algebras. Warshall asks whether a route exists; Floyd–Warshall asks for the cheapest route.

ComponentWarshallFloyd–Warshall
Cell valueBoolean reachabilityBest-known distance
Alternative routesOR $\lor$minimum $\min$
Join path segmentsAND $\land$addition $+$
Absent routefalse$+\infty$
Output question“Can $i$ reach $j$?”“What is the cheapest cost?”

If an all-pairs distance matrix has already been computed and no negative-cycle ambiguity applies, finite distance implies reachability. When reachability is the only goal, the Boolean formulation is simpler and particularly amenable to word-level bitset acceleration.

Implementation

The three-language versions expose a reflexive switch. They copy reachability into an $n\times n$ Boolean matrix, then update it in place with $k$ outermost. The early if reach[i][k] guard skips an entire row operation when $i$ cannot use $k$ as a bridge.

def warshalls_algorithm(n, edges, reflexive=True):
    """
    n: number of vertices (0-indexed)
    edges: list of (u, v) directed edges
    reflexive=True includes zero-edge paths i -> i.
    Returns an n x n boolean reachability matrix.
    """
    reach = [[False] * n for _ in range(n)]

    if reflexive:
        for i in range(n):
            reach[i][i] = True
    for u, v in edges:
        reach[u][v] = True

    for k in range(n):
        for i in range(n):
            if reach[i][k]:
                for j in range(n):
                    reach[i][j] = reach[i][j] or reach[k][j]

    return reach

# Example: 1->2->3->1 (cycle), plus 3->4  (0-indexed: 0->1->2->0, 2->3)
n = 4
edges = [(0, 1), (1, 2), (2, 0), (2, 3)]
result = warshalls_algorithm(n, edges, reflexive=False)

print("Reachability matrix:")
for row in result:
    print([int(x) for x in row])
#include <iostream>
#include <utility>
#include <vector>

using namespace std;

vector<vector<bool>> warshallsAlgorithm(
        int n, const vector<pair<int,int>>& edges, bool reflexive = true) {
    vector<vector<bool>> reach(n, vector<bool>(n, false));

    if (reflexive)
        for (int i = 0; i < n; ++i) reach[i][i] = true;
    for (const auto& e : edges) reach[e.first][e.second] = true;

    for (int k = 0; k < n; k++) {
        for (int i = 0; i < n; i++) {
            if (reach[i][k]) {
                for (int j = 0; j < n; j++) {
                    reach[i][j] = reach[i][j] || reach[k][j];
                }
            }
        }
    }
    return reach;
}

int main() {
    int n = 4;
    vector<pair<int,int>> edges = {{0,1}, {1,2}, {2,0}, {2,3}};
    auto reach = warshallsAlgorithm(n, edges, false);

    cout << "Reachability matrix:" << endl;
    for (auto& row : reach) {
        for (bool b : row) cout << b << " ";
        cout << endl;
    }
    return 0;
}
import java.util.*;

public class WarshallsAlgorithm {
    public static boolean[][] solve(int n, int[][] edges, boolean reflexive) {
        boolean[][] reach = new boolean[n][n];

        if (reflexive)
            for (int i = 0; i < n; ++i) reach[i][i] = true;
        for (int[] e : edges) reach[e[0]][e[1]] = true;

        for (int k = 0; k < n; k++) {
            for (int i = 0; i < n; i++) {
                if (reach[i][k]) {
                    for (int j = 0; j < n; j++) {
                        reach[i][j] = reach[i][j] || reach[k][j];
                    }
                }
            }
        }
        return reach;
    }

    public static void main(String[] args) {
        int n = 4;
        int[][] edges = {{0,1}, {1,2}, {2,0}, {2,3}};
        boolean[][] reach = solve(n, edges, false);

        System.out.println("Reachability matrix:");
        for (boolean[] row : reach) {
            System.out.println(Arrays.toString(row));
        }
    }
}

Bitset Acceleration

The scalar inner loop says: if row $i$ reaches $k$, merge every true destination from row $k$ into row $i$. A bitset stores an entire row in machine words, turning $V$ Boolean OR operations into a handful of word-level ORs. In Python, one arbitrary-precision integer can act as the row bitset.

def warshall_bitset(n, edges, reflexive=True):
    rows = [0] * n
    if reflexive:
        for i in range(n):
            rows[i] |= 1 << i
    for u, v in edges:
        rows[u] |= 1 << v

    for k in range(n):
        bit_k = 1 << k
        row_k = rows[k]
        for i in range(n):
            if rows[i] & bit_k:
                rows[i] |= row_k
    return rows

def reaches(rows, source, target):
    return bool(rows[source] & (1 << target))

Bit packing does not change the information-theoretic requirement: the full answer still contains $V^2$ bits. On a word-RAM with word size $w$, it reduces the update work toward $O(V^3/w)$ word operations and stores the matrix in $O(V^2/w)$ words. Actual speedup depends on language, representation, graph density, and cache behavior.

Store a Path Witness, Not Only a Boolean

A true matrix cell proves existence but does not explain the route. Add a nextHop matrix. For a direct edge $u\to v$, the first hop is $v$. When $(i,j)$ becomes reachable through $k$, copy the first hop used to reach $k$ from $i$.

def warshall_with_witness(n, edges, reflexive=True):
    reach = [[False] * n for _ in range(n)]
    next_hop = [[None] * n for _ in range(n)]

    if reflexive:
        for i in range(n):
            reach[i][i] = True
            next_hop[i][i] = i
    for u, v in edges:
        reach[u][v] = True
        next_hop[u][v] = v

    for k in range(n):
        for i in range(n):
            if not reach[i][k]:
                continue
            for j in range(n):
                if not reach[i][j] and reach[k][j]:
                    reach[i][j] = True
                    next_hop[i][j] = next_hop[i][k]
    return reach, next_hop

def reconstruct_path(next_hop, source, target):
    if next_hop[source][target] is None:
        return []
    path = [source]
    while source != target:
        source = next_hop[source][target]
        path.append(source)
    return path

The first witness discovered is retained; a different vertex order may produce a different valid route. This matrix reconstructs paths between distinct endpoints and the zero-edge reflexive witness $[i]$. If you need an explicit nonempty cycle for a true strict-diagonal cell, retain predecessor information or start reconstruction from one of the cycle's outgoing edges.

Complexity and When to Use It

$$\text{Scalar time}=O(V^3),\qquad \text{matrix space}=O(V^2).$$

The cubic loop is independent of $E$ after initialization, making Warshall attractive when the graph is dense and most or all of the $V^2$ reachability answers will be queried. It is often wasteful when the graph is sparse or only a few sources matter.

NeedApproachTypical cost
All-pairs closure, dense/moderate graphWarshall$O(V^3)$ time, $O(V^2)$ space
All-pairs closure with packed rowsBitset WarshallAbout $O(V^3/w)$ word operations
Reachability from one sourceDFS or BFS$O(V+E)$
All sources in a sparse graphDFS/BFS from every source$O(V(V+E))$
Strongly connected components onlyTarjan or Kosaraju$O(V+E)$
Graph changes frequentlyDynamic reachability strategyDepends on update/query pattern

Real-World Applications

Precomputed closure is most useful when the relation changes rarely but indirect-membership questions arrive frequently.

Hierarchies

Answer whether one role, class, category, or organizational unit is transitively below another.

Dependencies

Determine whether changing component $i$ can indirectly affect component $j$.

Recursive relations

Materialize ancestor, reporting-chain, or prerequisite relationships for repeated queries.

Structural Derivation

Strongly Connected Sets from Mutual Reachability

After strict or reflexive closure, vertices $u$ and $v$ belong to the same strongly connected component exactly when $R[u][v]$ and $R[v][u]$ are both true. This offers a simple matrix-based characterization, although linear-time SCC algorithms are far better when components—not all-pairs reachability—are the only desired output.

Access ControlType SystemsImpact AnalysisRecursive Queries

Pitfalls and Implementation Checklist

Common Failure Modes

  • Hiding the diagonal convention: decide whether zero-edge paths count and expose the choice.
  • Moving $k$ inside another loop: the DP proof depends on processing allowed intermediates in stages.
  • Using a shallow 2D copy: in Python, [[False] * n] * n aliases every row. Use a comprehension.
  • Expecting distances or path counts: a Boolean cell records existence only.
  • Ignoring updates to the source graph: any added or removed edge can invalidate many closure cells.
  • Paying cubic cost for a handful of queries: use targeted DFS/BFS when only a few sources matter.
  • Assuming a false strict diagonal means isolation: it means “not on a directed cycle,” not “cannot reach anyone.”

Validate the Result

Every direct edge must remain true; reachability must be transitive; reflexive mode must have a true diagonal; strict diagonal entries should correspond exactly to cycle membership; and randomized small graphs should agree with DFS/BFS from every source.

Exercises

  1. Run the four-vertex example in both diagonal modes and explain the only cell that differs.
  2. Prove the recurrence by induction on the set of allowed intermediate vertices.
  3. Find a counterexample for the loop order for i, for j, for k.
  4. Use mutual reachability to group the example's strongly connected components.
  5. Extend the witness version to return one explicit directed cycle for each true strict-diagonal cell.
  6. Benchmark scalar and bitset versions on dense and sparse graphs of increasing size.
  7. Challenge: after adding one new edge $a\to b$ to an existing closure, derive which rows and columns can be updated without recomputing from scratch.

Historical Note

Stephen Warshall published the Boolean-matrix closure algorithm in 1962; closely related work by Bernard Roy appeared earlier. The same three-loop dynamic-programming pattern is also associated with Floyd's all-pairs shortest-path formulation. The family is a classic example of one recurrence surviving while its value domain and operators change.