Senior 8 min · March 17, 2026

Cycle Detection — Why Two-Color DFS Fails on DAGs

Two-color DFS produces false cycle positives when cross edges exist in directed graphs.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Cycle detection finds paths that start and end at the same node
  • Directed graphs: DFS with three-color marking (white→gray→black) catches back edges
  • Undirected graphs: Union-Find checks if edge endpoints are already connected
  • Linked lists: Floyd's tortoise-and-hare uses O(1) space, no recursion stack
  • Performance: DFS is O(V+E), Union-Find is O(E α(V)), Floyd's is O(n)
  • Production gotcha: Forgetting gray-to-gray edges vs gray-to-black edges causes false positives in directed graphs

Cycle detection comes up in dependency resolution (circular imports), task scheduling (deadlock detection), and graph algorithms (topological sort only works on DAGs). The approach differs between directed and undirected graphs, and the distinction matters — what looks like a cycle in an undirected graph might not be one in a directed graph. Here's the thing: get the color semantics wrong and you'll flag a cross edge as a cycle. That's a production-level bug that's quiet — your topological sort just silently fails.

What is Cycle Detection? — Plain English

A cycle in a graph is a path that starts and ends at the same node. Cycle detection is important in many algorithms: topological sort requires an acyclic graph (DAG), dependency resolution must detect circular dependencies, and deadlock detection in operating systems finds cycles in resource-allocation graphs. For directed graphs, we use DFS with three-color marking (white=unvisited, gray=in current path, black=done). For undirected graphs, Union-Find is simpler. For linked lists, Floyd's cycle detection (tortoise and hare) uses O(1) space.

Here's the intuition: a cycle in a directed graph means you can follow edges and end up back where you started — that's a back edge in DFS language. In an undirected graph, any edge that connects two already-connected vertices creates a cycle — that's what Union-Find checks. And in a linked list, a cycle means following next pointers will loop forever — Floyd's algorithm catches that with no extra memory.

Graph vs List Cycles
  • Directed graph: Back edge in DFS (gray node found again).
  • Undirected graph: Union-Find finds that two nodes are already in the same set before adding the edge.
  • Linked list: Two pointers moving at different speeds will eventually meet if a cycle exists.
  • Two colors are never enough for directed graphs — the gray/black distinction is what separates a back edge from a harmless cross edge.
Production Insight
The most common production bug is using a two-color visited array on a directed graph.
You'll flag cross edges as cycles, blocking valid deployments and causing false alerts.
Rule: always pass the graph type (directed/undirected) explicitly to your cycle detection function.
Key Takeaway
Graph type dictates algorithm.
Directed = DFS with three colors.
Undirected = Union-Find or DFS with parent.
Linked list = Floyd's.
Two colors break directed detection.
Which Algorithm Should You Use?
IfGraph is directed
UseUse DFS with three-color marking
IfGraph is undirected, number of edges is large (E > V)
UseUse Union-Find for O(E α(V)) time and O(V) space
IfGraph is undirected but you already have adjacency list
UseDFS with parent tracking is fine (O(V+E)) but recursion stack may overflow for deep graphs
IfData structure is a singly linked list
UseUse Floyd's tortoise-and-hare — O(n) time, O(1) space
IfNeed to find all cycles, not just existence
UseUse Johnson's algorithm or Tarjan's strongly connected components for directed graphs

How Cycle Detection Works — Step by Step

Directed graph cycle detection using DFS coloring: 1. Initialize all nodes as WHITE (unvisited). 2. For each WHITE node, start a DFS. 3. Mark node as GRAY (currently being explored) before visiting neighbors. 4. If we reach a GRAY neighbor, a back edge exists → cycle detected → return True. 5. After exploring all neighbors, mark node as BLACK (fully explored). 6. If DFS completes without finding a gray neighbor, no cycle through this path.

Undirected graph cycle detection using Union-Find: 1. For each edge (u,v), check if u and v are already in the same component (find(u) == find(v)). 2. If yes, adding this edge creates a cycle → return True. 3. If no, union the components and continue. 4. If all edges are processed without triggering step 2, no cycle exists.

Linked list cycle detection (Floyd's): 1. slow = fast = head. 2. Each step: slow moves 1, fast moves 2. 3. If fast or fast.next is None → no cycle. 4. If slow == fast → cycle exists.

Don't skip the initialization: Union-Find needs each node's parent to point to itself. For linked lists, make sure both pointers start at head — a common mistake is to start slow at head and fast at head.next, which still works but requires extra care with null checks.

Production Insight
Recursion depth in DFS can blow the call stack for graphs with >10^5 nodes.
Switching to iterative DFS with an explicit stack avoids this.
Union-Find with path compression never uses recursion — it's safe for large graphs.
Floyd's is always iterative — no recursion risk.
Key Takeaway
Know the three algorithms by heart.
Directed: DFS coloring (3 states).
Undirected: Union-Find (edge endpoints in same set).
Linked list: Floyd's (two pointers).
Implement each with edge cases in mind: null pointers, recursion limits, parent tracking.

Worked Example — Tracing Directed Graph Cycle Detection

Directed graph: 0→1→2→3→1 (cycle: 1→2→3→1). Edges: (0,1),(1,2),(2,3),(3,1).

DFS from 0: 1. Visit 0: color[0]=GRAY. Explore neighbor 1. 2. Visit 1: color[1]=GRAY. Explore neighbor 2. 3. Visit 2: color[2]=GRAY. Explore neighbor 3. 4. Visit 3: color[3]=GRAY. Explore neighbor 1. 5. Node 1 is GRAY → BACK EDGE found! Cycle detected. Return True.

Undirected example: edges (0,1),(1,2),(2,0). Union-Find: process (0,1): find(0)=0, find(1)=1, different → union. Process (1,2): different → union. Process (2,0): find(2)=0, find(0)=0, SAME → cycle detected!

Now trace Floyd's on a linked list: 1→2→3→4→5→3 (cycle from 3). slow=1, fast=1 Step1: slow=2, fast=3 Step2: slow=3, fast=5 Step3: slow=4, fast=3 (fast moves from 5→3 via cycle) Step4: slow=5, fast=5 → meet at node 5. Cycle detected. To find the cycle start, reset slow=head (1), keep fast at 5, then move both one step each. They meet at node 3 — the cycle start.

Recursion Depth in Large Graphs
The DFS example uses recursion. In production graphs with >10^4 nodes, the recursion depth may exceed the Python recursion limit (default ~1000). Always implement iterative DFS or increase sys.setrecursionlimit() if you must use recursion.
Production Insight
The worked example assumes a connected graph.
If the graph is disconnected, you must iterate over all vertices and start DFS from each unvisited node.
Missing this step is a common cause of false negatives — you think no cycle exists because you only explored one component.
Always loop: for v in range(V): if color[v]==WHITE and dfs(v): return True.
Key Takeaway
Trace examples build muscle memory.
Directed: follow colors — gray means current path.
Undirected: Union-Find tracks components.
Linked list: fast & slow pointers meet.
Always consider disconnected graphs in your loop.

Visualizing the DFS Tree and Back Edge in Directed Graphs

In a directed graph, a DFS traversal produces a spanning tree (the DFS tree) composed of tree edges. Edges that are not part of the tree fall into three categories: back edges (points to an ancestor in the DFS tree), forward edges (points to a descendant), and cross edges (points to a node in a different branch). Only back edges indicate a cycle because they create a circular path from descendant back to ancestor.

Consider the graph 0→1→2→3→1. The DFS tree when starting at 0
  • Tree edges: 0→1, 1→2, 2→3
  • Back edge: 3→1 (node 1 is an ancestor of 3 in the tree)

The back edge 3→1 completes the cycle 1→2→3→1. In contrast, if a cross edge 3→0 existed, it would not create a cycle because 0 is not on the current recursion stack (it's black). The diagram below illustrates the DFS tree with the back edge highlighted.

Production Insight
When debugging, print the recursion stack at the moment a back edge is found.
The stack from root to current node contains all gray nodes; the back edge target is one of them.
In production logging, mark each node with its current color to distinguish tree edges from back edges.
This visual distinction is what saves you from false positive cycle reports.
Key Takeaway
Back edges are the only cycle indicators in directed DFS.
Tree edges go to white nodes, forward/cross edges go to black nodes, back edges go to gray nodes.
The recursion stack (gray nodes) is the current DFS path — a back edge always connects the current node to an ancestor on that stack.

Cycle Extraction — Retrieving the Actual Cycle Vertices

Detecting a cycle is often not enough; you also need to output the vertices that form the cycle. In directed DFS, when you encounter a back edge to a gray node, the cycle consists of the nodes on the recursion stack from that gray node back to the current node. By maintaining a stack of the current DFS path, you can extract the cycle as soon as a back edge is found.

Implementation approach
  • Use a recursion stack list that stores nodes in the order of the current DFS path.
  • When we discover a back edge to a node v that is already in the recursion stack, we copy the portion of the stack from the index of v to the end (the current node). That sublist is the cycle.
  • This works because the DFS recursion stack tracks the exact path from the root to the current node; any back edge to an ancestor closes a cycle along that path.

For undirected graphs, Union-Find can be extended to reconstruct the cycle by tracking parent edges during union. A simpler method: run DFS with parent tracking and, when a visited non-parent neighbor is found, trace back using the parent array to record the cycle. The parent array records the previous node on the current DFS path.

io/thecodeforge/graph/CycleExtraction.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package io.thecodeforge.graph;

import java.util.*;

public class CycleExtraction {
    static final int WHITE = 0, GRAY = 1, BLACK = 2;
    List<Integer> cycle = new ArrayList<>();
    int[] color;
    int[] parent;
    List<Integer>[] adj;
    Deque<Integer> stack = new ArrayDeque<>(); // recursion stack

    public boolean hasCycleWithExtraction(List<Integer>[] adj) {
        this.adj = adj;
        int n = adj.length;
        color = new int[n];
        parent = new int[n];
        Arrays.fill(parent, -1);
        for (int i = 0; i < n; i++) {
            if (color[i] == WHITE) {
                if (dfs(i)) return true;
            }
        }
        return false;
    }

    private boolean dfs(int u) {
        color[u] = GRAY;
        stack.push(u);
        for (int v : adj[u]) {
            if (color[v] == GRAY) {
                // Back edge; extract cycle from stack
                cycle.clear();
                Iterator<Integer> it = stack.descendingIterator();
                while (it.hasNext()) {
                    int node = it.next();
                    cycle.add(node);
                    if (node == v) break;
                }
                Collections.reverse(cycle);
                return true;
            } else if (color[v] == WHITE) {
                parent[v] = u;
                if (dfs(v)) return true;
            }
        }
        stack.pop();
        color[u] = BLACK;
        return false;
    }

    public List<Integer> getCycle() {
        return cycle;
    }
}
Cycle Extraction vs. Detection
Extracting the actual cycle costs no extra time but requires storing the recursion stack. For extremely large graphs, the stack memory overhead is O(V). If you only need existence, skip the cycle extraction and save memory. In production, log the cycle for debugging and alerting.
Production Insight
In a microservice dependency graph, knowing which services form the cycle is critical to breaking it.
The extracted cycle gives you exactly the edges to remove or reorder.
In job scheduling, printing the cycle vertices tells the DevOps engineer which jobs depend on each other circularly.
Store the cycle in a structured log field (e.g., JSON array) for easy alert correlation.
Key Takeaway
A cycle is not just a boolean — it's a list of vertices that form the loop.
Maintain a recursion stack during DFS; when a back edge is found, slice the stack from the target node to the current node.
This gives you the exact cycle path for debugging and resolution.

BFS-Based Cycle Detection for Undirected Graphs

While Union-Find is elegant, BFS (or DFS) with parent tracking is another common approach for undirected graphs. BFS uses a queue and processes nodes level by level. The key insight: in an undirected graph, a cycle exists if, during BFS, we encounter a visited neighbor that is NOT the parent of the current node. Since the graph is undirected, each edge is traversable in both directions; the parent relationship prevents us from treating the edge we just came from as a cycle.

Algorithm: 1. Initialize visited array (boolean). 2. For each unvisited node, start BFS. 3. Mark node as visited and set its parent to -1. 4. For each neighbor of the current node: - If not visited, mark visited, set parent = current node, enqueue. - If visited and neighbor != parent, a cycle is detected. 5. If BFS completes without finding such a neighbor, no cycle in that component.

Time complexity: O(V+E) — same as DFS, but avoids recursion stack depth. Space: O(V) for the queue and parent array. For very wide graphs, BFS may use more queue memory than DFS stack but avoids stack overflow. BFS is particularly useful when you also want shortest paths or level information.

io/thecodeforge/graph/BFSCycleDetection.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package io.thecodeforge.graph;

import java.util.*;

public class BFSCycleDetection {

    public static boolean hasCycleUndirected(List<Integer>[] adj) {
        int n = adj.length;
        boolean[] visited = new boolean[n];
        int[] parent = new int[n];
        Arrays.fill(parent, -1);

        for (int start = 0; start < n; start++) {
            if (visited[start]) continue;
            Queue<Integer> queue = new LinkedList<>();
            queue.add(start);
            visited[start] = true;
            while (!queue.isEmpty()) {
                int u = queue.poll();
                for (int v : adj[u]) {
                    if (!visited[v]) {
                        visited[v] = true;
                        parent[v] = u;
                        queue.add(v);
                    } else if (v != parent[u]) {
                        // visited and not parent -> cycle
                        return true;
                    }
                }
            }
        }
        return false;
    }
}
BFS vs. DFS for Undirected Cycle Detection
Both BFS and DFS with parent tracking work in O(V+E) time. BFS uses a queue (typically larger memory for wide graphs) but avoids recursion depth issues. DFS uses recursion (or explicit stack) and can find cycles faster if the graph is deep. For production systems with huge graphs, BFS is preferable because it doesn't risk stack overflow and is easier to reason about with iterative code.
Production Insight
BFS cycle detection is a good fit for social network graphs or any undirected graph where stack overflow is a risk.
It integrates naturally with BFS-based shortest-path computations (e.g., six degrees of separation).
If you already have a BFS traversal for another purpose, reuse the visited and parent arrays — the cycle check adds negligible overhead.
One gotcha: ensure you skip the parent check at the start node's first neighbor (parent is -1, so it won't match anything).
Key Takeaway
BFS with parent tracking detects cycles in undirected graphs without recursion.
A visited non-parent neighbor signals a cycle.
Same time complexity as DFS but safer for large graphs due to iterative queue usage.

C++ Implementations for Directed and Undirected Cycle Detection

C++ is widely used in performance-critical graph processing (e.g., game engines, trading systems). Here we provide two functions: one for directed graphs using three-color DFS, and one for undirected graphs using DFS with parent tracking. Both use adjacency lists and standard containers.

The directed variant uses a vector of int for colors: 0=WHITE, 1=GRAY, 2=BLACK. When a gray neighbor is found, a cycle is detected. The undirected variant uses a boolean visited array and an integer parent array; a cycle is detected when a visited neighbor is not the parent. Both functions are iterative in the sense that the recursion is depth-first (C++ compilers handle recursion well, but we also provide iterative alternatives in comments).

io/thecodeforge/graph/CycleDetection.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <vector>
#include <list>

enum Color { WHITE, GRAY, BLACK };

// Directed graph: returns true if cycle found
bool hasCycleDirected(const std::vector<std::list<int>>& adj) {
    int n = adj.size();
    std::vector<Color> color(n, WHITE);
    std::function<bool(int)> dfs = [&](int u) -> bool {
        color[u] = GRAY;
        for (int v : adj[u]) {
            if (color[v] == GRAY) return true;      // back edge
            else if (color[v] == WHITE && dfs(v)) return true;
        }
        color[u] = BLACK;
        return false;
    };
    for (int i = 0; i < n; ++i)
        if (color[i] == WHITE && dfs(i))
            return true;
    return false;
}

// Undirected graph: returns true if cycle found
bool hasCycleUndirected(const std::vector<std::list<int>>& adj) {
    int n = adj.size();
    std::vector<bool> visited(n, false);
    std::vector<int> parent(n, -1);
    std::function<bool(int)> dfs = [&](int u) -> bool {
        visited[u] = true;
        for (int v : adj[u]) {
            if (!visited[v]) {
                parent[v] = u;
                if (dfs(v)) return true;
            } else if (v != parent[u]) {
                return true;  // visited non-parent
            }
        }
        return false;
    };
    for (int i = 0; i < n; ++i)
        if (!visited[i] && dfs(i))
            return true;
    return false;
}
Recursion vs. Iterative in C++
The above uses recursion, which is fine for graphs up to ~10^5 nodes (default stack size ~8MB on Linux). For larger graphs, convert to iterative DFS using std::stack<std::pair<int, std::list<int>::iterator>> to simulate the call stack. The non-recursive version is longer but avoids stack overflow.
Production Insight
In C++ production systems, prefer iterative DFS to avoid stack overflow and to have predictable memory usage.
The recursive lambda above is concise for tutorials but should be replaced with an explicit stack in real code.
Also, use adjacency lists of ints (no extra overhead) and pass by const reference.
For undirected graphs, Union-Find (Disjoint Set Union) is often faster because it processes edges in any order and uses path compression.
Key Takeaway
C++ implementations mirror Java logic but use standard library containers.
Directed: three-color DFS, undirected: DFS with parent.
For production, convert to iterative to avoid stack overflow, or use Union-Find for undirected.

Advantages and Disadvantages of Each Algorithm

Each cycle detection algorithm has strengths and weaknesses. The table below summarizes the key trade-offs to help you choose the right tool for your production scenario.

AlgorithmAdvantagesDisadvantages
DFS (3-color)Works for both directed and undirected with slight modification; can extract cycle path; O(V+E) timeRecursion risk; O(V) recursion stack; must track parent for undirected; gray/black logic is subtle
Union-FindO(E α(V)) near-linear time; O(V) space; no recursion; very simple for undirected; good for dynamic edge additionsOnly for undirected; cannot detect cycles in directed graphs; cannot extract cycle path easily; requires edge list processing
Floyd's (Tortoise and Hare)O(1) space; O(n) time; no recursion; can find cycle start; works on linked listsOnly for linked lists; cannot be used for general graphs; requires two pointer management; not applicable to arrays or adjacency lists
BFS with parentAvoids recursion; O(V+E) time; natural for undirected graphs; integrates with BFS-based algorithmsQueue memory can be large for wide graphs; only for undirected; cannot extract cycle path easily (though parent array can be traced)

For production systems, the choice often comes down to graph type and memory constraints. If your graph is directed and large, iterative DFS with three colors is the gold standard. For large undirected graphs, Union-Find is usually the fastest and most memory-efficient. For linked lists, Floyd's is the only sensible choice.

When to Use BFS over Union-Find for Undirected Graphs
Union-Find is simpler for static graphs processed offline. BFS with parent is useful when you are already doing a BFS for other reasons (e.g., level, shortest path). If you only need cycle detection and the graph is static, choose Union-Find. If you need other BFS metadata, use BFS and add the cycle check.
Production Insight
In a real-time system where graph edges are added incrementally (e.g., dependency injection container), Union-Find is ideal because it supports dynamic unions without re-running the entire traversal.
DFS-based approaches require re-traversal after each edge addition.
Thus, choose Union-Find for dynamic undirected graphs, and iterative DFS for directed graphs where edges are added rarely.
Key Takeaway
No single algorithm fits all. Directed → DFS. Undirected → Union-Find or BFS with parent. Linked List → Floyd's. Each has distinct memory, recursion, and dynamic-graph trade-offs.

Practice Problems — Apply Cycle Detection Techniques

Sharpen your skills with these curated LeetCode and coding platform problems. They cover all three graph types and force you to choose the right algorithm. Attempt them after implementing the techniques above.

  1. LeetCode 207: Course Schedule — Directed graph cycle detection using three-color DFS or Kahn's algorithm (BFS topo).
  2. Hint: If a cycle exists, you can't finish all courses. Use adjacency list and detect back edges.
  3. LeetCode 210: Course Schedule II — Return an order of courses (topological sort) if possible, else empty array.
  4. Hint: Combine cycle detection with ordering. Kahn's algorithm works well here.
  5. LeetCode 141: Linked List Cycle — Classic Floyd's tortoise-and-hare.
  6. Hint: Use two pointers. Also try LeetCode 142 for cycle start.
  7. LeetCode 684: Redundant Connection — Undirected graph with n nodes and n edges. Find the edge that creates a cycle.
  8. Hint: Union-Find processes edges; the first edge whose endpoints are already connected is the answer.
  9. LeetCode 802: Find Eventual Safe States — Directed graph; nodes that are not part of any cycle are safe.
  10. Hint: Reverse the graph and do DFS with three colors to find nodes that are in cycles.
  11. LeetCode 1192: Critical Connections in a Network — Find edges whose removal disconnects the graph (bridges).
  12. Hint: Use DFS with discovery and low-link values (Tarjan's algorithm). Cycle detection helps identify non-bridge edges.
  13. LeetCode 133: Clone Graph — Deep copy a graph with possible cycles.
  14. Hint: Use DFS/BFS with a map from old to new node. Handle cycles by checking the map before recursing.

Start with Problems 1,3,4 to build confidence, then move to 2,5,6,7 for deeper understanding.

Systematic Problem-Solving
For each problem, first identify the graph type (directed/undirected) and the goal (detect cycle / find cycle edges / find safe nodes). Then choose the corresponding algorithm from this article. Implement the simplest correct solution first, then optimize if needed.
Production Insight
These problems mirror real-world scenarios: dependency resolution (207), network redundancy (684), and safe state analysis (802). In production, you might use Union-Find for undirected network analysis or three-color DFS for build dependency graphs. Practice with these to internalize the patterns.
Key Takeaway
Cycle detection is not just theoretical. LeetCode 207, 141, and 684 directly translate to real systems. Implement each algorithm in your language of choice and test against these problems to build fluency.
● Production incidentPOST-MORTEMseverity: high

False Cycle in a Microservice Dependency Graph

Symptom
Graph algorithm detecting a cycle in a service dependency DAG that multiple senior engineers confirmed was acyclic.
Assumption
The graph is undirected, so any visited neighbor that isn't the parent indicates a cycle.
Root cause
The team implemented DFS for directed graphs using a two-color scheme (visited/unvisited). They treated any already-visited neighbor as a cycle, but in a directed graph, a visited neighbor that is not on the current recursion path (a cross edge) is perfectly valid.
Fix
Switch to three-color marking: white = unvisited, gray = on current DFS path, black = fully explored. Only a gray neighbor triggers a cycle.
Key lesson
  • Always use three colors for directed graph cycle detection — two colors always produce false positives when cross edges exist.
  • Document the graph type explicitly in the algorithm: is it directed or undirected? The algorithm choice depends on it.
  • Add unit tests with known cyclic and acyclic graphs that include cross edges.
Production debug guideSymptom → Action for common failures when detecting cycles in dependency graphs, databases, and job schedulers.4 entries
Symptom · 01
Topological sort returns an error claiming a cycle, but you're sure the graph is acyclic.
Fix
Check if the graph is directed. If yes, verify you're using three-color DFS (white, gray, black). Print the recursion stack when a gray node is hit — the back edge is the cycle.
Symptom · 02
Union-Find reports a cycle in an undirected graph, but the graph seems fine.
Fix
Inspect the order of edge processing. Union-Find processes edges sequentially; if you process edges in a different order, the union decisions change. Ensure you're processing all edges and that the graph is truly undirected (no self-loops).
Symptom · 03
Linked list cycle detection returns false negative — no cycle found, but profiler shows infinite iteration.
Fix
Verify the pointer movement: slow=slow.next, fast=fast.next.next. If any node has next pointing to null, it's fine. If the list has odd length and fast.next is null, stop. Also check that both pointers start at head, not head.next.
Symptom · 04
Graph has millions of nodes and DFS crashes with stack overflow.
Fix
Use iterative DFS with explicit stack to avoid recursion depth limits. For Union-Find, path compression iteration keeps stacks shallow. For Floyd's, no recursion is used — it's inherently iterative.
★ Cycle Detection Quick Debug Cheat SheetThree common cycle detection algorithms and their immediate debug commands when something goes wrong.
DFS cycle detection returning false positives (directed graph)
Immediate action
Check color assignment: only gray neighbors should trigger cycle. If you're using two colors, switch to three.
Commands
print(recursion_stack) # output nodes in current DFS path
assert all(colors[g] != GRAY for g in neighbors) or raise CycleDetected
Fix now
Replace visited[neighbor] check with colors[neighbor] == GRAY only.
Union-Find missing a cycle in undirected graph+
Immediate action
Verify find() with path compression is implemented correctly. Check for self-loops or duplicate edges.
Commands
for u,v in edges: print(find(u), find(v))
union(u,v) # if find(u)==find(v), cycle exists
Fix now
Add debug logging in union function: if find(a)==find(b): log('Cycle detected via edge', a, b).
Floyd's algorithm not detecting cycle in linked list+
Immediate action
Check that both pointers start at head. Ensure fast pointer moves two steps only if fast.next exists.
Commands
while fast and fast.next: slow=slow.next; fast=fast.next.next; if slow==fast: return True
return False # if loop exits without meeting
Fix now
Add a counter to limit iterations to 2*list_length to avoid infinite loops during testing.
Cycle Detection Algorithms at a Glance
AlgorithmApplies ToTimeSpaceCycle Path?Recursion?
DFS (3-color)DirectedO(V+E)O(V) stackNo (just detect)Yes (recursive)
Union-FindUndirectedO(E α(V))O(V)No (just detect)No
Floyd'sLinked ListO(n)O(1)Yes (start node)No
Iterative DFSDirectedO(V+E)O(V) explicit stackYes (if tracking)No

Key takeaways

1
Back edges in DFS indicate cycles in directed graphs; gray nodes in the current path are back-edge targets.
2
Union-Find detects cycles in undirected graphs by checking if edge endpoints are already in the same component.
3
Floyd's tortoise-and-hare uses O(1) space for linked list cycle detection.
4
Always pass the graph type (directed/undirected) to the detection function
mixing them breaks correctness.
5
For large graphs, use iterative DFS to avoid recursion stack overflow; Union-Find and Floyd's are inherently iterative.

Common mistakes to avoid

5 patterns
×

Using two colors (visited/unvisited) for directed graph cycle detection

Symptom
DFS returns True (cycle detected) for graphs that are actually acyclic, especially when cross edges exist. This causes false positives in dependency resolution and topological sort.
Fix
Switch to three colors: white (unvisited), gray (in current DFS path), black (fully processed). Only a gray neighbor indicates a back edge and a cycle.
×

Forgetting to pass graph type to the detection function

Symptom
The function assumes undirected and uses Union-Find on a directed graph, missing cycles because it doesn't account for direction. Or it uses directed DFS on an undirected graph, flagging back edges incorrectly.
Fix
Explicitly pass a boolean 'directed' parameter. Document the graph type. Separate functions for directed and undirected graphs.
×

Not handling disconnected graphs in DFS

Symptom
DFS only starts from one node. If the graph has multiple components, cycles in other components are missed — false negative.
Fix
Always iterate over all vertices. For each unvisited node (white), run DFS. If any DFS returns true, there's a cycle.
×

Union-Find without path compression or union by rank

Symptom
find() becomes O(n) in worst case, leading to O(n^2) overall. For large graphs (millions of edges), the algorithm becomes unusably slow.
Fix
Implement path compression in find() (set parent[x] = find(parent[x]) recursively or iteratively) and union by rank (attach smaller tree to root of larger tree).
×

Starting Floyd's pointers incorrectly

Symptom
If both start at head, algorithm works. But some implementations start slow=head and fast=head.next, which skips the first node and can cause null pointer exceptions if head is null or has only one node.
Fix
Standard: slow=head, fast=head. Check fast!=null && fast.next!=null in loop condition. Do not start at different positions.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How do you detect a cycle in a directed graph? Explain the three-color a...
Q02SENIOR
How does Union-Find detect cycles in an undirected graph? What is the ti...
Q03SENIOR
Explain Floyd's cycle detection algorithm for linked lists and how to fi...
Q04SENIOR
What is the difference between a back edge and a cross edge in DFS? Why ...
Q01 of 04SENIOR

How do you detect a cycle in a directed graph? Explain the three-color algorithm.

ANSWER
Use DFS with three colors: white (unvisited), gray (currently in recursion stack), black (fully processed). Start DFS from each white node. When exploring a node, mark it gray. Recursively visit its neighbors. If you encounter a gray neighbor, that's a back edge — cycle detected. After visiting all neighbors, mark the node black. If DFS finishes without hitting a gray neighbor, no cycle in that path. This is required because a visited neighbor that is black (processed in a different DFS branch) is perfectly fine — only gray indicates a cycle.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Why use three colors (white/gray/black) instead of just visited/unvisited for directed graphs?
02
Why doesn't the simple visited set work for undirected graphs in cycle detection?
03
How does Floyd's algorithm find the cycle start after detecting a cycle?
04
Can I use DFS for undirected graph cycle detection without Union-Find?
05
What if my graph is both directed and undirected in different parts?
🔥

That's Graphs. Mark it forged?

8 min read · try the examples if you haven't

Previous
Minimum Spanning Tree — Kruskal and Prim
10 / 17 · Graphs
Next
Bipartite Graph Check