Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 57

Cycle Detection in Undirected Graphs with DFS

Recommended resources

Hello! Welcome back to our series on graph algorithms.

In previous lessons, we've explored how to traverse graphs with BFS and DFS and how to find the shortest path in weighted graphs using Dijkstra's algorithm. These are foundational skills for navigating graph structures. Today, we'll shift our focus from finding paths to identifying a key structural property: cycles.

Your learning outcome for this lesson is to detect cycles in undirected graphs using Depth-First Search (DFS). This is a classic interview problem that serves as a building block for more complex tasks, such as determining if a graph is a tree or finding redundant connections in a network.

1. The Challenge of Undirected Cycles

In a DFS traversal, the fundamental sign of a cycle is encountering a node that has already been visited. However, in an undirected graph, this simple rule has a catch.

Consider an edge between nodes A and B. When your DFS moves from A to B, node A is marked as visited. From B, one of its neighbors is A. Since the edge is undirected, you can immediately travel back to A. A is already visited, but A -> B -> A is not what we consider a cycle; it's just traversing an edge back and forth.

Undirected Graph with Tree and Back Edges for Cycle Detection
CAPTION: This image illustrates the edges explored during a DFS traversal. 'Tree Edges' form the DFS spanning tree. A 'Back Edge', like the one from node 3 to node 0, connects a node to an ancestor that is not its immediate parent. The presence of a back edge indicates a cycle.

So, how do we distinguish a true cycle from simply returning to the node we just came from?

The solution is to keep track of the parent of the current node in the DFS traversal. The "parent" is the node from which we arrived at the current node.

The rule for detecting a cycle then becomes:

During a DFS traversal, if we encounter a neighbor that is already visited and is not the immediate parent of the current node, we have found a cycle.

This parent tracking is the key modification to the standard DFS algorithm for this problem. The article "Detect Cycle in a Graph" explains this distinction clearly.

Detect Cycle in a Graph. Perform Depth First Search(DFS)…

This article from Medium contributor Yumin Lee highlights a crucial point: why the 'white-grey-black' set method (often used for directed graphs) fails for undirected graphs, and why tracking the parent is the correct approach.

Read the section titled 'Detect Cycle in an Undirected Graph', focusing on the paragraphs under 'White-Grey-Black Cycle Detection Does Not Work...' and 'Approach: DFS + Visited Nodes + Parent Node'. This will solidify why the parent-tracking method is necessary.


2. The DFS Algorithm with Parent Tracking

Let's see this logic in action. The core idea is to augment our recursive DFS function to accept two arguments: the current node and its parent.

To get a complete picture of the algorithm, including a step-by-step dry run, watch the following video.

G-12. Detect a Cycle in an Undirected Graph using DFS | C++ | Java

This video from 'take U forward' provides an excellent, detailed walkthrough of the entire process. It covers the intuition, a manual dry run, the algorithm's pseudocode, and how to handle graphs with multiple disconnected components.

Watch from 00:56 to 11:37. Focus on: The core intuition of using a parent node (00:56 - 01:54). The detailed dry run showing how a cycle is found when a visited, non-parent node is encountered (01:54 - 08:26). The pseudocode that formalizes this logic (08:26 - 11:37).

As the video demonstrates, the algorithm can be summarized as follows:

  1. Initialize a visited array (or set) for all vertices.
  2. Create a main function that iterates through all vertices from 0 to V-1. This handles disconnected graphs (graphs with multiple components).
  3. If a vertex i has not been visited, start a DFS from it. The initial call will be dfs(i, -1), as the starting node has no parent.
  4. The recursive dfs(node, parent) function does the following:
    • Mark node as visited.
    • For each neighbor of node:
      • If neighbor has not been visited, recursively call dfs(neighbor, node). If that call returns true (meaning a cycle was found deeper in the traversal), immediately propagate true up the call stack.
      • If neighbor has been visited, check if neighbor != parent. If this is true, you've found a back edge. Return true.
    • If the loop completes without finding a cycle, return false.
Test your understanding!

Consider this undirected graph represented by an adjacency list:
graph = {0:[1, 2], 1:[0, 3], 2:[0, 3], 3:[1, 2]}

You start a DFS from node 0 (dfs(0, -1)). The traversal path is 0 -> 1 -> 3. When you are at node 3, what are its neighbors? Which one will trigger the cycle detection logic and why?

Show answer

At node 3, the neighbors are 1 and 2.

  • The parent of 3 is 1. When the algorithm checks neighbor 1, it sees 1 is visited, but it is also the parent (neighbor == parent), so it does nothing.
  • When the algorithm checks neighbor 2, it sees that node 2 is also visited (from the path 0 -> 2). Since 2 is not the parent of 3 (2 != 1), the condition (visited[neighbor] and neighbor != parent) is met. This detects the cycle 0-1-3-2-0, and the function returns true.

3. Python Implementation and Complexity

Your background in Python and your goal of preparing for interviews make it crucial to have a clean, standard implementation ready. The following resource provides exactly that.

Cycle Detection in Undirected Graph using DFS - Tutorial

This tutorial page, also from 'takeuforward.org', provides concise code implementations in several languages. We will focus on the Python version, which serves as a perfect template for interviews.

Review the 'Algorithm' section for a quick text summary, then carefully study the Python code provided in the 'Code' section. Note how the main isCycle function handles disconnected components and how the dfs helper function implements the parent-checking logic.

Here's the Python implementation, which directly mirrors the logic we've discussed:

class Solution:
    # V: number of vertices
    # adj: adjacency list
    def isCycle(self, V, adj):
        visited = [False] * V

        # Loop to handle disconnected components
        for i in range(V):
            if not visited[i]:
                # If DFS from this node finds a cycle, return True
                if self.dfs(i, -1, adj, visited):
                    return True
        
        # If no component has a cycle
        return False

    def dfs(self, node, parent, adj, visited):
        visited[node] = True
        
        for neighbor in adj[node]:
            # If neighbor is not visited, recurse
            if not visited[neighbor]:
                if self.dfs(neighbor, node, adj, visited):
                    return True
            # If neighbor is visited and is NOT the parent
            elif neighbor != parent:
                # Cycle detected
                return True
        
        # No cycle found from this path
        return False

Complexity Analysis

Understanding the performance of your algorithms is non-negotiable in an interview setting.

  • Time Complexity:

    • Where is the number of vertices and is the number of edges.
    • The DFS traversal visits every vertex and every edge exactly once across all components. The outer loop ensures we cover disconnected parts of the graph, but the total work is still proportional to the total number of vertices and edges.
  • Space Complexity:

    • visited array: This takes space.
    • Recursion Stack: In the worst-case scenario (a long, path-like graph), the depth of the recursion can be up to . Therefore, the call stack requires space.
    • The total space complexity is dominated by these factors, resulting in .

The "take U forward" video also provides a good summary of this analysis.

G-12. Detect a Cycle in an Undirected Graph using DFS | C++ | Java

To reinforce the complexity analysis, let's briefly revisit the 'take U forward' video.

Watch the final section on complexity from 17:01 to 18:40.


4. Application: Validating a Tree

A very common interview problem that builds directly on this concept is "Graph Valid Tree" (LeetCode 261). A graph is a valid tree if and only if two conditions are met:

  1. The graph contains no cycles.
  2. The graph is fully connected (i.e., it has exactly V-1 edges and all nodes are reachable from a single source).

You now know how to solve the first part of this problem.

Graph Valid Tree - Leetcode 261 - Python

The NeetCode video for 'Graph Valid Tree' is a great example of applying this pattern. It uses the exact same dfs(node, previous) logic to detect cycles.

Watch the explanation of the algorithm from 03:18 to 06:13. Notice how he describes the need for a 'previous' node parameter to avoid false positives—this is the same as our 'parent' node logic.


Conclusion

In this lesson, you've added a crucial graph algorithm to your interview arsenal. Detecting cycles is a fundamental problem, and the DFS-based approach for undirected graphs is the standard, efficient solution.

Key Takeaways:

  • A simple "is visited?" check is not enough to detect cycles in an undirected graph because of bidirectional edges.
  • The correct approach is to use DFS while tracking the parent of each node in the traversal path.
  • A cycle is detected when you encounter a visited neighbor that is not the node's immediate parent.
  • To handle disconnected graphs, a loop in the main function must attempt a DFS from every unvisited vertex.
  • The algorithm runs in time and requires space.

Preview of the Next Lesson:

What about cycles in directed graphs? The parent trick won't work there, because an edge A -> B doesn't imply an edge B -> A. An edge from a node to a previously visited node could be a cross-edge to a different branch, not necessarily a back-edge forming a cycle. In the next lesson, we will explore how to detect cycles in directed graphs using DFS, which requires a more nuanced three-state tracking system (visiting, visited, unvisited).