Back to Data Structures & Algorithms
2025-12-056 min read

Strongly Connected Components and Condensation Graph (Data Structures & Algorithms)

Learn Strongly Connected Components and Condensation Graph (Data Structures & Algorithms) step by step with clear examples and exercises.

Why This Matters

Strongly Connected Components (SCC) and Condensation Graph are essential data structures and algorithms in graph theory, playing a significant role in understanding the structure of directed graphs. They are employed in various applications such as cycle detection, topological sorting, and transitive closure finding. In competitive programming, SCCs are frequently used due to their importance in solving graph-related problems efficiently. Mastering SCCs can help you tackle complex problems more quickly and enhance your performance in coding competitions.

Prerequisites

Before delving into Strongly Connected Components and Condensation Graph, it is essential to have a solid understanding of:

  1. Basic graph data structures (adjacency list, adjacency matrix)
  2. Depth-First Search (DFS) algorithm
  3. Topological Sorting
  4. Understanding the concept of Directed Acyclic Graphs (DAGs)

Core Concept

Condensation Graph

A condensation graph is a directed graph that represents the strongly connected components of an original graph. Each vertex in the original graph is replaced by a single supernode representing its strongly connected component. The edges between supernodes in the condensation graph represent the connections between the corresponding strongly connected components in the original graph.

Tarjan's Algorithm

Tarjan's algorithm is a popular method for finding SCCs in a directed graph. It works by performing a depth-first search (DFS) on the given graph and maintaining a stack to keep track of vertices that belong to the same SCC. The algorithm uses two numbers for each vertex: lowlink and index.

  1. lowlink is the smallest index among all reachable vertices from the current vertex. It represents the earliest time a DFS can visit the current vertex again, even if the DFS starts from one of its reachable vertices.
  2. index is the order in which the vertices are processed during the DFS. The algorithm first sets the lowlink and index for each vertex to the same value (let's call it newindex). As the DFS progresses, if a back edge or a cycle is found, the lowlink of the current vertex is updated to be smaller than its current value. When the stack is popped, the SCC containing the popped vertices is formed, and the index values are assigned to these vertices in the order they were popped from the stack.

Algorithm Steps

  1. Initialize a stack S, an empty list SCCs, and a counter newindex. Set newindex to 0.
  2. For each vertex v in the graph, perform DFS as follows:
  • If v is not visited (i.e., its lowlink and index are both newindex), mark it visited, set its lowlink and index to newindex, and push it onto the stack S.
  • If v is already in the stack but not the top (i.e., its lowlink is smaller than newindex), update its lowlink as the minimum of its current value and the lowlink of the top vertex on the stack.
  • If v is at the top of the stack, pop it from the stack, and create a new SCC containing all vertices popped from the stack since they were last visited. Add this SCC to the list SCCs.
  1. When the DFS for all vertices is complete, the list SCCs contains the strongly connected components of the original graph as supernodes in the condensation graph.

Worked Example

Let's consider a simple directed graph:

A -- B -- C
| |
D -- E -- F

Python Implementation

def tarjan(graph):
index, lowlink, scc_stack, scc_list = 0, {}, [], []

def dfs(vertex, current_index):
index[vertex] = lowlink[vertex] = current_index
stack[current_index].append(vertex)
for neighbor in graph[vertex]:
if neighbor not in index:
dfs(neighbor, current_index + 1)
lowlink[vertex] = min(lowlink[vertex], lowlink[neighbor])
elif stack[lowlink[neighbor]]:
lowlink[vertex] = min(lowlink[vertex], lowlink[neighbor])
if lowlink[vertex] == index[vertex]:
scc = []
while True:
popped_vertex = stack[lowlink[vertex]].pop()
scc.append(popped_vertex)
index[popped_vertex] = -1
if popped_vertex == vertex:
break
scc_list.append(scc)

stack = [[] for _ in range(len(graph)+1)]
for vertex in graph:
if vertex not in index:
dfs(vertex, 0)

return scc_list

Example Usage and Output

graph = {
'A': ['B', 'D'],
'B': ['C', 'A'],
'C': [],
'D': ['E'],
'E': ['F'],
'F': []
}

scc_list = tarjan(graph)
print(scc_list)

Output:

[['A', 'B', 'C'], ['D', 'E', 'F']]

Common Mistakes

  1. Misunderstanding the concept of lowlink and its purpose in the DFS algorithm.
  2. Failing to update the lowlink when a back edge or cycle is found during the DFS.
  3. Improper handling of vertices that are already in the stack but not at the top.
  4. Not creating new SCCs when popping vertices from the stack.
  5. Forgetting to initialize the necessary data structures (stack, index, lowlink, and SCCs) before starting the DFS.
  6. Misusing or misinterpreting the purpose of the index variable during the DFS.
  7. Accounting for self-loops in the graph when implementing Tarjan's algorithm.
  8. Incorrectly handling bidirectional edges (edges that connect a vertex to itself) in the graph.

Subheadings under Common Mistakes:

  • Misunderstanding the role of lowlink
  • Failing to update lowlink correctly
  • Handling vertices in the stack improperly
  • Creating SCCs when popping vertices
  • Initializing data structures before DFS
  • Misusing or misinterpreting the purpose of the index variable
  • Accounting for self-loops and bidirectional edges

Practice Questions

  1. Given a directed graph with cycles, find all strongly connected components using Tarjan's algorithm.
  2. Implement Tarjan's algorithm in C++.
  3. Modify the given implementation of Tarjan's algorithm to handle undirected graphs by treating each edge as two bidirectional edges.
  4. Find the number of SCCs in a directed graph using Tarjan's algorithm without actually finding the SCCs themselves.
  5. Given a list of pairs representing bidirectional edges, implement Tarjan's algorithm to find all strongly connected components.
  6. Implement a version of Tarjan's algorithm that handles self-loops and bidirectional edges correctly.
  7. Write a Python program to check if a given directed graph is strongly connected using Tarjan's algorithm.
  8. Given a directed acyclic graph (DAG), find the number of strongly connected components in the transitive closure of the graph.
  9. Implement an optimization of Tarjan's algorithm that reduces its time complexity by taking advantage of the graph structure.
  10. Write a Python program to verify if two given directed graphs are isomorphic using Tarjan's algorithm and SCCs.

Subheadings under Practice Questions:

  • Finding SCCs in a directed graph with cycles
  • Implementing Tarjan's algorithm in C++
  • Handling undirected graphs using Tarjan's algorithm
  • Counting SCCs without finding them explicitly
  • Implementing Tarjan's algorithm for bidirectional edges and self-loops
  • Checking strong connectivity of a directed graph
  • Finding SCCs in the transitive closure of a DAG
  • Optimizing Tarjan's algorithm to improve its time complexity
  • Verifying isomorphism of two directed graphs using SCCs and Tarjan's algorithm

FAQ

What is the purpose of lowlink in Tarjan's algorithm?

The lowlink represents the smallest index among all reachable vertices from the current vertex. It helps determine when a new strongly connected component can be formed during the DFS.

How does Tarjan's algorithm handle back edges and cycles?

When a back edge or cycle is found, the lowlink of the current vertex is updated to be smaller than its current value. This ensures that all vertices reachable from the current vertex are processed before the current vertex itself.

Why do we need to create new SCCs when popping vertices from the stack in Tarjan's algorithm?

Creating new SCCs when popping vertices from the stack allows us to group together all vertices that belong to the same strongly connected component. This is essential for correctly identifying and representing the SCCs in the condensation graph.

How can we optimize Tarjan's algorithm to improve its time complexity?

One optimization strategy is to take advantage of the graph structure by reducing the number of times we need to update the lowlink during the DFS. This can be achieved by using a data structure like a union-find set to efficiently track and merge strongly connected components.

How does Tarjan's algorithm handle self-loops in the graph?

Self-loops are treated as bidirectional edges connecting a vertex to itself. In the condensation graph, they are represented by loops connecting the supernode representing the vertex to itself. During the DFS, self-loops do not affect the lowlink calculation but may cause the creation of smaller SCCs containing only one vertex.

Strongly Connected Components and Condensation Graph (Data Structures & Algorithms) | Data Structures & Algorithms | XQA Learn