Transitive reduction
algorithm
pseudocode
graph theory
computer science

transitive reduction algorithm pseudocode?

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

The transitive reduction of a directed graph is the smallest graph that has the same reachability as the original — if vertex u can reach vertex v in the original graph, it can also reach v in the reduction, and no edge can be removed without losing reachability. Transitive reduction is the inverse of transitive closure and is used to simplify dependency graphs, remove redundant edges in DAGs, and visualize minimal relationships. For a DAG, the transitive reduction is unique.

Definitions

  • Reachability: Vertex u can reach vertex v if there is a directed path from u to v
  • Transitive closure: Add a direct edge u → v for every pair where u can reach v indirectly
  • Transitive reduction: Remove every edge u → v where u can still reach v through other edges

Example: Given edges A → B, B → C, A → C, the edge A → C is redundant because A can reach C via B. The transitive reduction removes A → C.

Pseudocode (Brute Force)

 
1function TransitiveReduction(G):
2    for each edge (u, v) in G.edges:
3        // Remove edge temporarily
4        G.remove_edge(u, v)
5
6        // Check if u can still reach v without this edge
7        if BFS(G, u, v) == reachable:
8            // Edge is redundant — keep it removed
9            continue
10        else:
11            // Edge is essential — restore it
12            G.add_edge(u, v)
13
14    return G

Time complexity: O(V * E) where V is vertices and E is edges, since each edge removal triggers a BFS/DFS taking O(V + E).

Pseudocode (Matrix-Based)

Using the adjacency matrix and transitive closure:

 
1function TransitiveReduction(A):
2    // A is the adjacency matrix (n × n)
3    // Compute transitive closure T using Warshall's algorithm
4    T = copy(A)
5    for k = 0 to n-1:
6        for i = 0 to n-1:
7            for j = 0 to n-1:
8                T[i][j] = T[i][j] OR (T[i][k] AND T[k][j])
9
10    // Remove redundant edges
11    R = copy(A)
12    for i = 0 to n-1:
13        for j = 0 to n-1:
14            if R[i][j] == 1:
15                for k = 0 to n-1:
16                    // If i can reach k directly and k can reach j,
17                    // then i→j is redundant
18                    if k != i AND k != j AND A[i][k] == 1 AND T[k][j] == 1:
19                        R[i][j] = 0
20                        break
21
22    return R

Time complexity: O(V^3) due to Warshall's algorithm.

Python Implementation

python
1from collections import defaultdict, deque
2
3def transitive_reduction(graph):
4    """
5    graph: dict mapping vertex -> set of neighbors
6    Returns the transitive reduction as a new graph.
7    """
8    reduced = {u: set(neighbors) for u, neighbors in graph.items()}
9
10    for u in graph:
11        for v in list(reduced[u]):
12            # Check if u can reach v without the direct edge u → v
13            reduced[u].discard(v)
14            if not can_reach(reduced, u, v):
15                reduced[u].add(v)  # Edge is essential
16
17    return reduced
18
19def can_reach(graph, start, target):
20    """BFS to check if start can reach target."""
21    visited = set()
22    queue = deque([start])
23    while queue:
24        node = queue.popleft()
25        if node == target:
26            return True
27        if node in visited:
28            continue
29        visited.add(node)
30        for neighbor in graph.get(node, []):
31            queue.append(neighbor)
32    return False
33
34# Example
35graph = {
36    'A': {'B', 'C', 'D'},
37    'B': {'C', 'D'},
38    'C': {'D'},
39    'D': set()
40}
41
42result = transitive_reduction(graph)
43print(result)
44# {'A': {'B'}, 'B': {'C'}, 'C': {'D'}, 'D': set()}
45# Removed: A→C, A→D, B→D (all reachable via other paths)

Using NetworkX

python
1import networkx as nx
2
3G = nx.DiGraph()
4G.add_edges_from([
5    ('A', 'B'), ('A', 'C'), ('A', 'D'),
6    ('B', 'C'), ('B', 'D'),
7    ('C', 'D')
8])
9
10# Compute transitive reduction
11TR = nx.transitive_reduction(G)
12print(list(TR.edges()))
13# [('A', 'B'), ('B', 'C'), ('C', 'D')]

nx.transitive_reduction() works on DAGs and raises an error for graphs with cycles.

Applications

  • Dependency management: In build systems (Make, Gradle), the transitive reduction shows the minimal set of direct dependencies needed
  • Database schema visualization: Simplifying foreign key relationships by removing redundant transitive references
  • Task scheduling: Identifying the essential ordering constraints in a task dependency graph
  • Version control: Simplifying commit ancestry graphs to show only direct parent relationships

Common Pitfalls

  • Applying to graphs with cycles: For DAGs, the transitive reduction is unique. For general directed graphs with cycles, the transitive reduction is not unique and the algorithm must handle strongly connected components separately. NetworkX's transitive_reduction rejects cyclic graphs.
  • Confusing transitive reduction with transitive closure: Transitive closure adds edges (makes all indirect reachability explicit). Transitive reduction removes edges (keeps only the essential ones). They are inverse operations.
  • Modifying the graph during edge iteration: Removing edges while iterating over them causes missed checks or errors. Copy the edge list before iterating, or collect edges to remove and apply them after the loop.
  • Not checking edge direction: Transitive reduction is defined for directed graphs. Applying it to undirected graphs requires converting to a DAG first (e.g., by choosing an ordering), which changes the problem semantics.
  • Assuming O(V + E) time complexity: The brute-force algorithm is O(V * E) because each of the E edges requires a BFS/DFS of O(V + E). The matrix-based approach is O(V^3). There is no known algorithm faster than O(V * E) for sparse graphs.

Summary

  • Transitive reduction removes redundant edges while preserving all reachability
  • For each edge u → v, check if u can reach v via other paths — if yes, remove the edge
  • The brute-force approach runs BFS/DFS per edge at O(V * E); the matrix approach uses Warshall's algorithm at O(V^3)
  • For DAGs, the transitive reduction is unique; for cyclic graphs, it is not
  • Use networkx.transitive_reduction() in Python for a ready-made implementation
  • Applications include dependency management, database visualization, and task scheduling

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.