Skip to main content

Welcome to a Grasp lesson

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

Lesson 23

Detecting Cycles with DFS

Recommended resources

Hello! Welcome to your next lesson on graph algorithms.

In previous lessons, you mastered the two fundamental graph traversal algorithms: Depth-First Search (DFS) and Breadth-First Search (BFS). You saw how DFS uses a stack (via recursion) to go deep into the graph, while BFS uses a queue to explore layer-by-layer.

Today, we'll build directly on your knowledge of DFS. Your learning outcome is to detect cycles in both directed and undirected graphs using DFS. A cycle is a path in a graph that starts and ends at the same vertex. Detecting them is crucial in many real-world software engineering scenarios, such as finding dependency loops in build systems, identifying deadlocks in concurrent processes, or validating workflows. This is a key skill for developing the "architectural mindset" you're aiming for.

We will tackle this in two parts, as the technique for detecting cycles is different for undirected and directed graphs.

1. Cycle Detection in Undirected Graphs

In an undirected graph, edges are two-way. A cycle is a closed loop where you can traverse from a node back to itself without immediately backtracking along the same edge you just used.

Undirected Graph with Identified Cycle
This image shows an undirected graph. The path 2-3-4-5-2 forms a cycle. Our goal is to create an algorithm that can detect such structures.

The basic idea is to use DFS and keep track of visited nodes. If our DFS traversal encounters a node that has already been visited, we might have found a cycle. However, there's a catch.

Consider a simple graph A -- B. If we start a DFS from A, we visit A, then move to its neighbor B. From B, one of its neighbors is A, which is already visited. Is this a cycle? No, it's just the two-way nature of the edge.

To solve this, our DFS algorithm needs to know not just which nodes are visited, but also how it arrived at the current node. We can do this by keeping track of the parent of each node in the traversal path.

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

This video provides an excellent, detailed walkthrough of this exact logic.

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

The channel 'take U forward' clearly explains the intuition behind using a 'parent' node to correctly identify cycles in undirected graphs. This walkthrough will solidify the algorithm in your mind.

Watch from 00:56 to 11:37. Pay close attention to how the parent parameter is passed in the recursive DFS calls and used to differentiate between a true back-edge (a cycle) and simply traversing back to the node you just came from.

The Algorithm in a Nutshell

For each vertex in the graph (to handle disconnected components):

  1. If the vertex has not been visited, start a recursive DFS from it. Pass an initial parent value that's impossible (like -1).
  2. In the recursive dfs(node, parent) function:
    a. Mark node as visited.
    b. For each neighbor of node:
    i. If neighbor has not been visited, recursively call dfs(neighbor, node). If this recursive call returns true (meaning a cycle was found deeper in the graph), immediately return true.
    ii. Else if the neighbor is not the parent, you've found a cycle! Return true.
  3. If the DFS completes for a component without returning true, no cycle was found in that component. If all components are checked and no cycle is found, the graph is acyclic.

Complexity

As this is essentially a standard DFS traversal, the complexity remains the same:

  • Time Complexity: , where V is the number of vertices and E is the number of edges. We visit each vertex and edge once.
  • Space Complexity: for the visited array and the recursion stack space in the worst case (for a path-like graph).
Test your understanding!

Consider this undirected graph:

  • 0: [1, 2]
  • 1: [0, 3]
  • 2: [0, 3]
  • 3: [1, 2]

Starting a DFS from node 0 (and exploring neighbors in increasing order), trace the algorithm. Does it find a cycle? If so, at which step is the cycle detected?

Show answer

Yes, a cycle is detected. The cycle is 0-1-3-2-0.

Trace:

  1. dfs(0, -1): Mark 0 as visited.
  2. Explore neighbors of 0. First is 1.
  3. dfs(1, 0): Mark 1 as visited.
  4. Explore neighbors of 1. First is 0. But 0 is the parent, so we ignore it. Next neighbor is 3.
  5. dfs(3, 1): Mark 3 as visited.
  6. Explore neighbors of 3. First is 1. But 1 is the parent, so we ignore it. Next neighbor is 2.
  7. dfs(2, 3): Mark 2 as visited.
  8. Explore neighbors of 2. First is 0.
    • Node 0 is visited.
    • Node 0 is not the parent of the current node (the parent of 2 is 3).
    • Cycle detected! The function dfs(2, 3) returns true. This true value propagates all the way up the call stack.

2. Cycle Detection in Directed Graphs

Now let's turn to directed graphs. Can we use the same parent-checking logic? The answer is no. The problem in directed graphs is different.

Directed Graph with a Cycle
This image shows a directed graph with a cycle 0 → 1 → 2 → 0. Notice the one-way edges.

Consider a graph where 1 -> 2 and 1 -> 3 -> 2. If we start DFS from 1, we might explore 1 -> 3 -> 2. Now nodes 1, 3, and 2 are marked visited. When we backtrack to 1 and explore the path 1 -> 2, we find that 2 is already visited. But this is not a cycle. It's just two different paths leading to the same node.

The key insight for directed graphs is that a cycle exists if and only if the DFS traversal encounters a "back edge"—an edge that leads from the current node to a node that is an ancestor in the current DFS traversal path.

To implement this, we need to track not just which nodes have been visited globally, but which nodes are currently in the recursion stack.

The Solution: We use two boolean arrays:

  1. visited[]: Tracks nodes that have been visited at any point. This prevents redundant processing of the same node across different DFS paths.
  2. pathVisited[] (also called onStack[] or recursionStack[]): Tracks nodes that are currently in the recursion path of the active DFS traversal.

The Rule: During a DFS, if we encounter a neighbor that is already in the pathVisited[] array, we have found a cycle.

This video first explains why the undirected method fails and then walks through the two-array solution.

G-19. Detect cycle in a directed graph using DFS | Java | C++

This video from 'take U forward' is crucial. It first demonstrates why the simpler parent-check fails for directed graphs, then introduces the two-array (visited and pathVisited) technique which is the standard solution.

Watch from the beginning to 12:17. The first part (until 04:39) explains the problem, and the rest details the correct algorithm with a clear step-by-step example. Focus on when a node is added to pathVisited and, critically, when it's removed.

The Algorithm in a Nutshell

  1. Initialize a visited array and a pathVisited array to all false.
  2. For each vertex v in the graph:
    a. If v is not visited, call dfs_check(v).
  3. In the recursive dfs_check(node) function:
    a. Mark node as true in both visited and pathVisited.
    b. For each neighbor of node:
    i. If neighbor is not visited, call dfs_check(neighbor). If it returns true, a cycle was found, so return true immediately.
    ii. Else if neighbor is already marked true in pathVisited, we've hit an ancestor in the current path. Cycle detected! Return true.
    c. Before the function returns, un-mark node in pathVisited (set it to false). This is the backtracking step, signifying that node is no longer in the current recursion path.
    d. Return false (no cycle found from this node).

For a good textual summary and Java code examples, this article is a helpful supplement.

Detecting Cycles in Graphs with Depth First Search in Java

This article from Medium provides a clear textual explanation of the logic, using the terms 'unvisited,' 'visiting' (our pathVisited), and 'visited.' Seeing the implementation in Java will be familiar to you as a software engineer.

Read the sections 'How Depth First Search Detects Cycles', 'Why Recursion Works for Directed Graphs', and 'Time and Space Complexity'. Notice how the onStack boolean array in their code serves the exact same purpose as the pathVisited array in the video.


Conclusion

You have now learned how to adapt DFS to solve the critical problem of cycle detection. The key is understanding that the definition of a "back edge" differs between undirected and directed graphs, requiring two distinct approaches.

Key Takeaways:

  • Cycle detection is a powerful application of DFS.
  • For undirected graphs, a cycle is found when DFS hits a visited node that is not its immediate parent. The algorithm is a DFS that passes a parent reference.
  • For directed graphs, a cycle is found when DFS hits a node that is currently in the recursion stack. The algorithm uses two arrays: visited and pathVisited (or onStack).
  • In both cases, the time complexity is because the underlying algorithm is still DFS.

Preview of the Next Lesson:
Our work with directed graphs and cycles leads directly to our next topic: Topological Sorting. This algorithm orders the vertices of a Directed Acyclic Graph (DAG) such that for every directed edge from vertex u to vertex v, u comes before v in the ordering. As the name implies, it only works on graphs with no cycles. You've just learned how to verify that prerequisite