Pattern #52

Cycle Detection

Important interview questions, thinking patterns, Undirected Parent Rule, Directed 3-State Array (0, 1, 2), Union Find Cycle Detection, Fast & Slow Pointers, and Go templates.

Must Solve

15 core questions — solve these first.

  1. 1.Detect Cycle in an Undirected Graph
    medium
  2. 2.Detect Cycle in a Directed Graph
    medium
  3. 3.Course Schedule
    medium
  4. 4.Course Schedule II
    medium
  5. 5.Graph Valid Tree
    medium
  6. 6.Redundant Connection
    medium
  7. 7.Find Eventual Safe States
    medium
  8. 8.Is Graph Bipartite?
    medium
  9. 9.Detect Cycle Using Union Find
    medium
  10. 10.Find the Duplicate Number
    medium
  11. 11.Circular Array Loop
    medium
  12. 12.Linked List Cycle
    easy
  13. 13.Linked List Cycle II
    medium
  14. 14.Alien Dictionary
    hard
  15. 15.Minimum Height Trees / Tree Validation Variants
    medium

Also Important

7 more questions worth practicing.

  1. 16.Find All Possible Recipes from Given Supplies
    medium
  2. 17.Parallel Courses
    medium
  3. 18.Remove Invalid Dependencies
    hard
  4. 19.Detect Cycle in Dependency Graph
    medium
  5. 20.Strongly Connected Components
    hard
  6. 21.Functional Graph Cycle Problems
    medium
  7. 22.Longest Cycle in a Graph
    hard

How to Think

  1. Undirected graph?DFS/BFS + Parent check (visited && neighbor != parent)
  2. Directed graph?DFS + 3-State Array (0=unvisited, 1=visiting, 2=finished)
  3. Edges arriving one by one?Union Find (Find(u) == Find(v) before union)
  4. Every node points to 1 next node?Fast & Slow Pointers (Floyd's algorithm)

Go Cycle Detection Templates

Undirected DFS Cycle, Directed 3-State DFS Cycle & Union Find Cycle in Go

// 1. Undirected Graph DFS Cycle Detection: O(V + E) Time
func hasCycleUndirected(n int, adj [][]int) bool {
    visited := make([]bool, n)
    var dfs func(u, parent int) bool
    dfs = func(u, parent int) bool {
        visited[u] = true
        for _, v := range adj[u] {
            if !visited[v] {
                if dfs(v, u) { return true }
            } else if v != parent {
                return true // Visited neighbor AND not parent -> Cycle!
            }
        }
        return false
    }

    for i := 0; i < n; i++ {
        if !visited[i] && dfs(i, -1) { return true }
    }
    return false
}

// 2. Directed Graph 3-State DFS Cycle Detection: O(V + E) Time
// States: 0 = Unvisited, 1 = Visiting (active stack), 2 = Finished
func hasCycleDirected(n int, adj [][]int) bool {
    state := make([]int, n)
    var dfs func(u int) bool
    dfs = func(u int) bool {
        if state[u] == 1 { return true }  // Back Edge Cycle!
        if state[u] == 2 { return false } // Fully processed -> Safe!

        state[u] = 1 // Mark visiting
        for _, v := range adj[u] {
            if dfs(v) { return true }
        }
        state[u] = 2 // Mark finished
        return false
    }

    for i := 0; i < n; i++ {
        if state[i] == 0 && dfs(i) { return true }
    }
    return false
}

// 3. Union Find Cycle Detection (Redundant Connection): O(E · α(V)) Time
func findRedundantConnection(edges [][]int) []int {
    n := len(edges)
    parent := make([]int, n+1)
    for i := 1; i <= n; i++ { parent[i] = i }

    var find func(i int) int
    find = func(i int) int {
        if parent[i] != i { parent[i] = find(parent[i]) }
        return parent[i]
    }

    for _, edge := range edges {
        u, v := edge[0], edge[1]
        rootU, rootV := find(u), find(v)
        if rootU == rootV {
            return edge // Endpoints already connected -> Redundant Cycle Edge!
        }
        parent[rootV] = rootU
    }
    return nil
}

👉 Undirected/Directed DFS: O(V + E) | Union Find: O(E · α(V)) ≈ O(E)

Undirected Parent Rule vs Directed 3-State Array

1. Undirected Parent Check: In an undirected graph, edge u - v goes both ways. Passing the parent node handles the edge you came from. Seeing a visited node where neighbor != parent reveals a cycle.

2. Directed 3-State Array: A simple boolean array fails for directed graphs! Reaching a finished node (state = 2) from a different branch is NOT a cycle. A cycle exists ONLY when an edge points to a node in the current active call stack (state = 1 Visiting).

Union Find & Fast & Slow Pointer Cycles

1. Union Find (Undirected Edges Arriving Incrementally): When processing edge u - v, if Find(u) == Find(v) before union, u and v are already connected. Adding edge u - v creates a cycle (Redundant Connection).

2. Functional Graph Fast & Slow Pointers: When every node has exactly 1 outgoing pointer (Linked List Cycle, Find Duplicate Number, Circular Array Loop), Floyd's Fast & Slow pointers (slow = 1 step, fast = 2 steps) detect cycles in O(1) space upon collision.

Visual Memory Rule
Undirected Cycle → Visited neighbor && neighbor != parent
Directed Cycle   → Edge to state 1 (Visiting / Active Stack)
3-State Array    → 0=unvisited, 1=visiting, 2=finished
Union Find Cycle → Find(u) == Find(v) before union
Fast & Slow      → Functional graph (out-degree 1) cycle collision

💡 Golden Rule: "In an undirected graph, ignore the edge you came from; in a directed graph, a cycle exists when you reach a node that is still inside the current DFS path."

Common Interview Mistakes

1. Using Parent Check on Directed Graph

The parent edge trick is exclusively for undirected graphs! Directed graphs require 3-state tracking (0, 1, 2).

2. Using 2-State Boolean Array for Directed Cycle

Reaching a finished node from an earlier branch is NOT a cycle. A cycle exists only when reaching a node currently visiting (state 1).

3. Forgetting Disconnected Components

A cycle might be located in component #3! Always run an outer loop over all 0..n-1 nodes.

4. Using Union Find for Directed Cycles

Standard Union Find does NOT preserve directed edge directions and will return false positive cycles on DAGs! Use 3-State DFS or Kahn's algorithm.

Interview Rules

  1. 1. Undirected graph cycle? → Visited + Parent check (visited[neighbor] && neighbor != parent)
  2. 2. Directed graph cycle? → 3-State Array (0=unvisited, 1=visiting, 2=finished). Edge to state 1 = cycle
  3. 3. Course Schedule / Dependency Deadlock?→ Directed Cycle Detection or Kahn's Topological Sort
  4. 4. Incremental undirected edges? → Union Find (Find(u) == Find(v) before union)
  5. 5. Functional graph (out-degree 1)?→ Floyd's Fast & Slow pointers (slow=1, fast=2)
  6. 6. Graph Valid Tree? → Components == 1 AND no cycles (or edges == n - 1)
  7. 7. Disconnected Graph? → Outer loop over all 0..n-1 nodes
  8. 8. Overall Complexity → Traversal Time: O(V + E) | Union Find Time: O(E · α(V))

Small Rules

  1. Rule 1: Undirected cycle requires ignoring the parent edge you arrived from.
  2. Rule 2: Directed cycle requires edge to a node in state 1 (visiting/active stack).
  3. Rule 3: State 2 (finished) nodes are fully processed and safe.
  4. Rule 4:Kahn's algorithm leaves unprocessed nodes when a cycle is present.
  5. Rule 5: Union Find detects undirected cycles when edge endpoints share a root.

Production Thinking

Microservice Dependency DeadlocksDetecting circular service call chains (A -> B -> C -> A) preventing startup deadlocks

Build System Dependency ChecksVerifying package dependency graphs (Bazel, npm, Cargo) are acyclic before compilation

Workflow Engine Cycle PreventionValidating approval workflow state machines to prevent infinite approval loops

Object Reference Memory LeaksDetecting cyclic object references in garbage collector graph sweeps

Remember This

Cycle Detection

Undirected       → Visited + Parent check
Visited != parent → Cycle

Directed         → 0 / 1 / 2 states
Edge to Visiting → Cycle

Kahn's BFS       → Nodes left unprocessed = Cycle
Union Find       → Same root before union = Cycle
Fast & Slow      → Functional graph (out-degree 1)
Time             → O(V + E)

💡 Golden Rule: "In an undirected graph, ignore the edge you came from; in a directed graph, a cycle exists when you reach a node that is still inside the current DFS path."