Skip to main content

Welcome to a Grasp lesson

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

Lesson 58

Cycle Detection in Directed Graphs using DFS

Recommended resources

Hello! Welcome to your next lesson on graph algorithms.

In our last session, we mastered how to detect cycles in undirected graphs using DFS by tracking a parent node. This was crucial to avoid mistaking a simple bidirectional edge traversal for a true cycle.

Today, we'll tackle a related but distinct challenge: detecting cycles in directed graphs using DFS. This is a fundamental problem with direct applications in areas like task scheduling, dependency resolution, and deadlock detection—making it a favorite in technical interviews.

As you'll soon see, the parent-tracking trick is no longer sufficient. Directed graphs require a more sophisticated way to distinguish between different types of edges encountered during a traversal.

1. Why the Parent-Tracking Method Fails for Directed Graphs

In an undirected graph, if we're at node B and see a visited neighbor A from which we just came, it's not a cycle. It's just the other direction of the same edge.

In a directed graph, an edge A -> B does not imply an edge B -> A. If our DFS path is C -> A -> B, and from B we see an edge B -> A, this is a true cycle (A -> B -> A). The concept of a "parent" in the traversal doesn't help us distinguish a legitimate cycle from other path types.

A more complex scenario arises with "cross-edges." Imagine our DFS explores one branch completely (e.g., 0 -> 1 -> 2), and then backtracks to explore another branch (0 -> 3). If there's an edge from 3 to the already-visited node 1, is this a cycle? No, it's a cross-edge connecting two separate branches of the DFS tree. A simple visited check would incorrectly flag this as a cycle.

To understand this distinction visually and intuitively, the following video provides an excellent explanation.

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

This video from 'take U forward' starts by perfectly illustrating why the algorithm for undirected graphs is not suitable here. It demonstrates a case where a DFS encounters a visited node that does not form a cycle.

Watch from the beginning to 04:39. Pay close attention to the example graph and the dry run. The key takeaway is understanding why visiting an already-visited node is not sufficient proof of a cycle in a directed graph. The video introduces the crucial concept of being 'on the same path'.


2. The Two-State Tracking System: visited and recursion stack

The core insight from the video is that we need to detect when we loop back to a node that is an ancestor in the current path of exploration. We need to differentiate between:

  1. A node visited in a previous, completed DFS branch.
  2. A node visited in the current, active DFS branch.

To achieve this, we use two boolean arrays (or sets):

  • visited[]: This array tracks every node that has ever been visited. Once a node is marked True, it stays True. This helps us avoid re-processing nodes and entire subgraphs that we know are cycle-free.
  • recursionStack[] (also called pathVisited[] or visiting[]): This array tracks only the nodes that are currently in the recursion call stack for the active DFS traversal. A node is marked True when we enter its DFS function and marked False right before we exit (i.e., after exploring all its neighbors).

The rule for detecting a cycle is now simple and robust:

If our DFS traversal from a node u encounters a neighbor v that is already in the recursionStack (recursionStack[v] == True), we have found a back edge. This indicates a cycle.

DFS-Based Cycle Detection in a Directed Graph
CAPTION: A directed graph with nodes 0 through 6. During a DFS starting from node 1, the path might proceed 1 -> 2 -> 3. From node 3, there's an edge back to node 1. At this point, node 1 is still in the recursion stack, so a cycle is detected.

The Three-Color Analogy

This two-array system is often described using a "three-color" analogy, which is great for conceptual understanding:

  • WHITE: Unvisited node. (visited[i] == False and recursionStack[i] == False)
  • GRAY: Node is currently being visited (in the recursionStack). (recursionStack[i] == True)
  • BLACK: Node and all its descendants have been fully visited. (visited[i] == True and recursionStack[i] == False)

A cycle is found whenever the DFS encounters a GRAY node.

The "take U forward" video continues with a fantastic, detailed walkthrough of this exact algorithm.

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

Continuing with the same video, we'll now see a full dry run of the two-array algorithm. It demonstrates how the visited and pathVisited arrays are updated during traversal and backtracking, first on a graph with no cycle, and then on one with a cycle.

Watch from 04:39 to 12:17. This is the most critical part of the lesson. 04:39 - 05:56: Introduction of the two arrays: visited and pathVisited. 05:56 - 10:43: A detailed walkthrough on a graph without a cycle, showing how pathVisited is marked false upon backtracking. 10:43 - 12:17: A walkthrough on a graph with a cycle, showing the exact moment a cycle is detected because a neighbor is found in the pathVisited array.


3. Python Implementation and a Classic Interview Problem

With the logic clear, let's look at a standard Python implementation. This pattern is essential for your interview toolkit. The code below is adapted from the common structure for this problem.

def isCyclic(V, adj):
    visited = [False] * V
    recursion_stack = [False] * V

    # This loop handles disconnected components in the graph
    for node in range(V):
        if not visited[node]:
            if dfs(node, visited, recursion_stack, adj):
                return True # Cycle found
    
    return False # No cycle found in any component

def dfs(node, visited, recursion_stack, adj):
    # Mark the current node as visited and part of the recursion stack
    visited[node] = True
    recursion_stack[node] = True

    # Recur for all the neighbors
    for neighbor in adj[node]:
        # If neighbor is not visited, then recurse on it
        if not visited[neighbor]:
            if dfs(neighbor, visited, recursion_stack, adj):
                return True
        # If the neighbor is in the recursion stack, then a cycle is present
        elif recursion_stack[neighbor]:
            return True

    # The node is removed from the recursion stack before the function returns
    # This is the backtracking step
    recursion_stack[node] = False
    return False

You can also find this implementation and further explanation in the GeeksForGeeks article "Detect Cycle in a Directed Graph".

Application: LeetCode 207 "Course Schedule"

This algorithm isn't just theoretical; it's the direct solution to one of the most common graph interview questions. The problem asks if you can finish all courses given a set of prerequisites. For example, to take course 1, you must first take course 0 ([1, 0]).

This is a directed graph problem where courses are nodes and prerequisites are edges. Can you finish all courses? Yes, if and only if the graph has no cycles. A cycle like 0 -> 1 -> 0 means you can never satisfy the prerequisites.

Course Schedule - Graph Adjacency List - Leetcode 207

The NeetCode video for 'Course Schedule' provides a concise explanation of how to apply our cycle detection algorithm to solve this exact problem. He uses a visitSet, which is functionally identical to our recursion_stack.

Watch from 01:50 to 02:47 to understand the problem setup, and then from 09:10 to 14:37 for the explanation of the DFS algorithm and the code walkthrough. This will solidify the connection between the abstract algorithm and a concrete, high-ROI interview problem.

Test your understanding!

Consider a graph with 4 courses and the following prerequisites: [[1,0], [2,1], [3,2], [1,3]]. This corresponds to the edges: 0->1, 1->2, 2->3, and 3->1.

Trace the dfs algorithm starting from node 0. At what point is the cycle detected? Which arrays (visited and recursion_stack) hold what values at that moment?

Show answer
  1. dfs(0) is called. visited[0] and recursion_stack[0] become True.
  2. From 0, dfs(1) is called. visited[1] and recursion_stack[1] become True.
  3. From 1, dfs(2) is called. visited[2] and recursion_stack[2] become True.
  4. From 2, dfs(3) is called. visited[3] and recursion_stack[3] become True.
  5. From 3, we look at its neighbor, 1.
  6. The algorithm checks neighbor 1. It sees visited[1] is True. The elif condition is checked: recursion_stack[1] is also True.
  7. Cycle detected! The path 1 -> 2 -> 3 -> 1 forms a cycle. The dfs(3) call returns True, which propagates all the way up.

At this moment, visited is [T, T, T, T] and recursion_stack is [T, T, T, T].


4. Complexity Analysis

For any algorithm, analyzing its performance is key.

  • Time Complexity: . This is the standard time complexity for a DFS traversal on a graph represented by an adjacency list. We visit each vertex (V) and each edge (E) at most once across all DFS calls.
  • Space Complexity: .
    • The visited and recursion_stack arrays each take space.
    • The depth of the recursion call stack can be at most in the worst case (a long chain).
    • Therefore, the total space is proportional to the number of vertices.

This analysis is also covered in the "take U forward" video from 15:30 to 16:15.


Conclusion

You've now learned the standard and efficient method for detecting cycles in directed graphs. This is a significant step up from the undirected case and a powerful pattern to have in your problem-solving toolkit.

Key Takeaways:

  • Detecting cycles in directed graphs requires distinguishing between back edges (which form cycles) and cross edges.
  • The standard approach uses a two-state system: one to track all visited nodes and another to track the recursion_stack (nodes in the current DFS path).
  • A cycle is found when DFS encounters a neighbor that is already present in the recursion_stack.
  • This algorithm runs in time and space and is the direct solution to popular interview questions like "Course Schedule."

Preview of the Next Lesson:

The concept of a cycle is fundamental to our next topic: Topological Sorting. A topological sort is a linear ordering of vertices such that for every directed edge u -> v, vertex u comes before vertex v in the ordering. Such an ordering is only possible if the graph is a Directed Acyclic Graph (DAG). In our next lesson, we will build upon today's work to implement algorithms that produce this ordering.