Welcome back. In our previous lessons, you mastered the two fundamental graph traversal algorithms, DFS and BFS, and even delved into a formal proof of correctness for BFS's shortest path property. This gives us a solid foundation to start solving more complex structural problems on graphs.
Today's lesson focuses on a direct and powerful application of Depth-First Search. We will explore the concept of connected components in an undirected graph. Your goal is to understand what they are and how to write an efficient algorithm to find and label them using DFS. This is a common subproblem in competitive programming, often the first step in analyzing a graph's structure before applying more advanced techniques.
What is a Connected Component?
Intuitively, a connected component of an undirected graph is a subgraph where you can get from any vertex to any other vertex by following a path of edges. The entire graph might be split into several such "islands" of vertices that are connected internally but are completely disconnected from each other.
To solidify this idea, the following video provides some excellent visual examples.
6.11 Connected Components |How to find Connected Components in Graph | Graph Theory
This video from Jenny's Lectures CS IT provides a clear, visual introduction to what connected components are.
Watch the initial segment from the beginning until the examples. The presenter shows how a single graph can be broken into one, two, or three distinct components by removing edges.
For a more rigorous definition, which aligns with your preference for theoretical underpinnings, we can define connectivity as an equivalence relation.
1 Connected components in undirected graphs 2 ...
These lecture notes from Stanford's CS161 course provide a formal definition of connected components.
Read the section titled "Connected components in undirected graphs". Notice how the "is reachable from" relationship (denoted ~) is reflexive, symmetric, and transitive, making it an equivalence relation. A connected component is simply an equivalence class under this relation.
The Algorithm: Using DFS to Find Components
So, how do we find these components algorithmically? The core idea is surprisingly simple and builds directly on the DFS you already know. A single DFS traversal starting from a source vertex s will visit every vertex reachable from s. In an undirected graph, this is precisely the set of vertices that belong to the same connected component as s.
This leads to a straightforward algorithm:
- Initialize a
visitedarray for all vertices tofalse. - Initialize a counter,
component_count, to 0. - Iterate through all vertices of the graph, from
v = 0ton-1. - For each vertex
v, if it has not been visited (!visited[v]), it means we have found a new, unexplored connected component. - Therefore, we increment
component_countand start a DFS traversal fromv. This DFS call will explore and mark as visited all vertices belonging to this new component. - When the main loop finishes,
component_countwill hold the total number of connected components.
The following image illustrates a graph with two connected components and provides pseudocode that captures this exact logic.
Let's trace the algorithm shown in the image on the sample graph:
- Initialization:
visitedarray is allfalse,CC = 0. - Outer loop
DFS(G):w = A:visited[A]is false. IncrementCCto 1. CallEXPLORE(A).EXPLORE(A): MarksAas visited (ccnum(A) = 1). It might exploreBnext.EXPLORE(B): MarksBas visited (ccnum(B) = 1). It might exploreD.- ... This process continues until
A, B, C, D, Eare all visited and marked as part of component 1. TheEXPLOREcalls then backtrack and finish. w = B:visited[B]is now true. Theifcondition is false, so we skip.- ... The loop skips
C,D, andEfor the same reason. w = F:visited[F]is false. IncrementCCto 2. CallEXPLORE(F).EXPLORE(F): MarksFas visited (ccnum(F) = 2). It exploresGandH, marking them as part of component 2.- ... The loop skips
GandH.
- End: The loop finishes. The algorithm has correctly identified 2 components and assigned each vertex to one.
The video below walks through this exact process, explaining how to assign a component ID to each node during the traversal.
Depth First Search Algorithm | Graph Theory
This video by WilliamFiset visually demonstrates the algorithm and shows how to adapt the basic DFS pseudocode to count and label components.
First, watch the conceptual explanation from finding connected components. Then, see how this translates to pseudocode in the following segment, pseudocode for components.
C++ Implementation and Proof of Correctness
Now let's look at a concrete implementation. The logic is a direct translation of the pseudocode we just discussed. We'll have a main function that iterates through all vertices and a recursive dfs helper function.
This article from cp-algorithms.com provides a concise explanation and a clean C++ implementation tailored for competitive programming.
First, read the section An algorithm for solving the problem for a recap of the logic. Then, examine the C++ code block that follows. Notice how the find_comps function contains the main loop, and it calls the dfs helper function for each new component it finds.
The find_comps function in the provided code is the driver. It uses a used vector (our visited array). The comp vector is used to store the nodes of the current component being explored. In many competitive programming problems, you might instead want a persistent array, say component_id[N], that stores the component number for each vertex, just like in the ccnum(w) pseudocode from the image.
A quick exercise for you: How would you modify the provided C++ code to populate a vector<int> component_id of size n instead of just printing the components?
Hint
You would need a counter variable in the `find_comps` function. Each time you enter the `if (!used[v])` block, you would increment this counter. Then, you would pass the current counter value to the `dfs` function, which would then assign `component_id[u] = current_component_id` for every node `u` it visits.Proof of Correctness
Why does this simple procedure work? We need to be sure that a single DFS call from an unvisited vertex v finds all vertices in its component and no vertices outside of it.
1 Connected components in undirected graphs 2 ...
This section of the Stanford notes provides a concise proof of correctness for the algorithm.
Read the short section "Algorithm to find connected components", focusing on the proof. It uses a simple but powerful inductive argument.
The proof formalizes our intuition.
- A traversal (DFS or BFS) starting at
vcan never reach a vertex outsidev's connected component, by the very definition of a component (no connecting path exists). - The traversal will reach every vertex inside
v's component. The proof shows this by induction on the distance (number of edges) fromv. It correctly reachesv(distance 0). If it reaches all vertices at distancei, it will also discover their neighbors at distancei+1, thus eventually reaching all vertices in the component.
Complexity Analysis and A Practical Note
Time Complexity: The main loop in find_comps iterates V times. However, the dfs function is only called on a vertex if it hasn't been visited yet. This ensures that each connected component is traversed exactly once. Over all the calls to dfs, each vertex is visited once, and each edge (u, v) is considered twice (once from u's adjacency list and once from v's). Therefore, the total time complexity is , which is linear in the size of the graph.
Space Complexity: The space required is dominated by the visited array and the recursion stack for DFS. Both are proportional to the number of vertices, so the space complexity is .
Recursive vs. Iterative DFS:
For competitive programming, it's worth noting a potential pitfall of recursive DFS. If a graph has a very long path (e.g., 1 -> 2 -> 3 -> ... -> 10^5), the recursion depth can become very large, potentially causing a stack overflow error. The cp-algorithms article you read briefly mentions this and shows an iterative version using an explicit std::stack. For most problems, the recursive version is cleaner and sufficient, but it's crucial to be aware of this limitation and the iterative alternative.
Conclusion
In this lesson, you have learned a fundamental graph algorithm that is both simple and efficient. By combining a simple loop with the DFS traversal you already know, you can now systematically partition any undirected graph into its constituent connected components.
Key Takeaways:
- A connected component is a maximal subgraph where every vertex is reachable from every other vertex.
- The algorithm to find all components involves iterating through all vertices and launching a DFS from any vertex that has not yet been visited.
- Each new DFS call from the main loop corresponds to discovering one new component.
- The algorithm is highly efficient, running in time.
- This technique can be used to either count the components or to assign a unique ID to each vertex indicating which component it belongs to.
You've now seen how to use traversals to find shortest paths (BFS) and to identify connected components (DFS). In our next lesson, we will continue augmenting DFS to solve another critical problem: detecting cycles in both undirected and directed graphs.