Cycle Detection
The Core Problem
A cycle is a path that starts and ends at the same vertex. Detecting cycles is essential for:
WHY CYCLE DETECTION MATTERS: Topological sort: Only works on DAGs (no directed cycles) Deadlock detection: Circular waiting = directed cycle in resource graph Infinite loops: Cycle in dependency graph = no valid build order BST validation: A proper BST has no cycles (it's a tree) THREE DISTINCT PROBLEMS: 1. Undirected graph — BFS with Pair(node, parent) 2. Undirected graph — DFS with parent parameter 3. Directed graph — DFS with pathVisited[] (current stack) 4. Directed graph — Kahn's BFS (in-degree approach) Approach 1 & 2 are equivalent (BFS vs DFS for undirected). Approach 3 & 4 are equivalent (DFS vs BFS for directed). The KEY difference is undirected vs directed — not BFS vs DFS.
Why Undirected and Directed Need Different Logic
UNDIRECTED GRAPH:
Edge (u,v) exists in BOTH adjacency lists.
When at node u (arrived from parent p):
Neighbour p will always appear as "visited" — it's where we came from.
We must skip p (it's our parent, not a back edge).
Any OTHER visited neighbour → real back edge → CYCLE.
CHECK: visited[nbr] AND nbr != parent
A ─── B
│ │
C ─── D ← 4-cycle: when DFS reaches D from C (parent=C),
D sees B as visited and B≠C → CYCLE ✓
DIRECTED GRAPH:
Edge u→v exists ONLY in u's list (not v's list).
When DFS finishes a node, it could be revisited from a different branch.
Revisiting a FINISHED node → cross edge → NOT a cycle.
Revisiting a node still ON the active path → back edge → CYCLE.
CHECK: pathVisited[nbr] (currently on recursion stack)
A → B → C
↑ |
└─────────┘ Cycle: C→A is visited AND A is still on active path
A → B → C
D ─────→ C No cycle: D→C reaches C which is FINISHED, not on stack
Approach 1: BFS Cycle Detection — Undirected Graph
Based on the reference Java implementation — Pair(node, parent) stored in the queue.
ALGORITHM:
For each unvisited vertex (outer loop for disconnected graphs):
Start BFS with Pair(start, -1) in queue.
Mark start visited.
While queue not empty:
Dequeue Pair(node, parent).
For each neighbour nbr of node:
If nbr not visited: enqueue Pair(nbr, node), mark visited.
Else if nbr != parent: CYCLE FOUND — return true.
Return false (no cycle).
1import java.util.*;
2
3// Pair stores (node, parent) — needed to skip the edge we came from
4class Pair {
5 int node, parent;
6 Pair(int node, int parent) {
7 this.node = node;
8 this.parent = parent;
9 }
10}
11
12public class CycleDetectionBFSUndirected {
13
14 // BFS for ONE component starting from 'start' — O(V + E)
15 public static boolean bfs(int start,
16 ArrayList<ArrayList<Integer>> graph,
17 boolean[] visited) {
18 Queue<Pair> q = new LinkedList<>();
19 q.offer(new Pair(start, -1));
20 visited[start] = true;
21
22 while (!q.isEmpty()) {
23 Pair current = q.poll();
24 int node = current.node;
25 int parent = current.parent;
26
27 for (int nbr : graph.get(node)) {
28 if (!visited[nbr]) {
29 visited[nbr] = true;
30 q.offer(new Pair(nbr, node)); // Store nbr's parent = node
31 } else if (nbr != parent) {
32 // nbr is visited AND is not the parent we came from
33 // → genuine back edge → CYCLE!
34 return true;
35 }
36 }
37 }
38 return false; // No cycle in this component
39 }
40
41 // Check entire graph (handles disconnected components)
42 public static boolean hasCycle(int V, ArrayList<ArrayList<Integer>> graph) {
43 boolean[] visited = new boolean[V];
44
45 for (int i = 0; i < V; i++) {
46 if (!visited[i]) {
47 if (bfs(i, graph, visited)) return true;
48 }
49 }
50 return false;
51 }
52
53 public static void main(String[] args) {
54 Scanner sc = new Scanner(System.in);
55 int V = sc.nextInt();
56 int E = sc.nextInt();
57
58 ArrayList<ArrayList<Integer>> graph = new ArrayList<>();
59 for (int i = 0; i < V; i++) graph.add(new ArrayList<>());
60
61 for (int i = 0; i < E; i++) {
62 int u = sc.nextInt();
63 int v = sc.nextInt();
64 graph.get(u).add(v);
65 graph.get(v).add(u); // Undirected
66 }
67
68 boolean[] visited = new boolean[V];
69 boolean flag = false;
70
71 for (int i = 0; i < V; i++) {
72 if (!visited[i]) {
73 if (bfs(i, graph, visited)) {
74 flag = true;
75 break;
76 }
77 }
78 }
79
80 System.out.println(flag ? "Cycle is Detected" : "Cycle is Not Detected");
81 }
82}
83
84/*
85Sample Input (cyclic):
864 4
870 1
881 2
892 3
903 0
91
92Output: Cycle is Detected
93
94Sample Input (acyclic):
954 3
960 1
971 2
982 3
99
100Output: Cycle is Not Detected
101*/Sample Input (cyclic): Sample Input (acyclic): 4 4 4 3 0 1 0 1 1 2 1 2 2 3 2 3 3 0 Output: Cycle is Detected Output: Cycle is Not Detected
Dry Run: BFS Cycle Detection (Undirected)
Graph (cyclic): 0─1─2─3─0 (square — has a cycle)
Adjacency list:
0: [1, 3]
1: [0, 2]
2: [1, 3]
3: [2, 0]
visited = [F, F, F, F]
Outer loop: i=0, not visited
Enqueue Pair(0, -1). visited[0]=T
visited = [T, F, F, F]
Dequeue Pair(0, -1): node=0, parent=-1
nbr=1: not visited → enqueue Pair(1,0), visited[1]=T
nbr=3: not visited → enqueue Pair(3,0), visited[3]=T
queue = [Pair(1,0), Pair(3,0)], visited=[T,T,F,T]
Dequeue Pair(1, 0): node=1, parent=0
nbr=0: visited AND 0==parent(0) → SKIP (same edge we came from)
nbr=2: not visited → enqueue Pair(2,1), visited[2]=T
queue = [Pair(3,0), Pair(2,1)], visited=[T,T,T,T]
Dequeue Pair(3, 0): node=3, parent=0
nbr=2: visited AND 2 ≠ parent(0) → CYCLE DETECTED! ✓
return true
Output: "Cycle is Detected"
Approach 2: DFS Cycle Detection — Undirected Graph
Same logic as BFS, but using the call stack. Pass parent as a parameter.
ALGORITHM:
dfs(node, parent, graph, visited):
visited[node] = true
for each nbr of node:
if not visited[nbr]: recursively dfs(nbr, node, ...)
else if nbr != parent: CYCLE (back edge to non-parent visited node)
return false
1import java.util.*;
2
3public class CycleDetectionDFSUndirected {
4
5 public static boolean dfs(int start, int parent,
6 ArrayList<ArrayList<Integer>> graph,
7 boolean[] visited) {
8 visited[start] = true;
9
10 for (int nbr : graph.get(start)) {
11 if (!visited[nbr]) {
12 if (dfs(nbr, start, graph, visited)) return true;
13 } else if (nbr != parent) {
14 // Visited AND not the node we came from → CYCLE
15 return true;
16 }
17 }
18 return false;
19 }
20
21 public static void main(String[] args) {
22 Scanner sc = new Scanner(System.in);
23 int V = sc.nextInt();
24 int E = sc.nextInt();
25
26 ArrayList<ArrayList<Integer>> graph = new ArrayList<>();
27 for (int i = 0; i <= V; i++) graph.add(new ArrayList<>());
28
29 for (int i = 0; i < E; i++) {
30 int u = sc.nextInt();
31 int v = sc.nextInt();
32 graph.get(u).add(v);
33 graph.get(v).add(u); // Undirected
34 }
35
36 boolean[] visited = new boolean[V + 1];
37 boolean flag = false;
38
39 for (int i = 1; i <= V; i++) {
40 if (!visited[i]) {
41 if (dfs(i, -1, graph, visited)) {
42 flag = true;
43 break;
44 }
45 }
46 }
47
48 System.out.println(flag ? "Cycle Detected" : "No Cycle Detected");
49 }
50}Same input/output as BFS approach — both detect cycles correctly. BFS uses Pair(node,parent) in queue. DFS passes parent as a function parameter.
Approach 3: DFS Cycle Detection — Directed Graph
For directed graphs, visited[] alone is insufficient. We need pathVisited[] (also called inStack[]) to track the current active DFS path.
WHY visited[] ALONE FAILS FOR DIRECTED GRAPHS:
A ──→ B ──→ C
↑
D ─────────┘ No cycle! D→C is a cross edge.
DFS from A: visits A, B, C (C marked visited).
DFS from D: visits D, then tries C.
C is visited → if we check visited[] alone → FALSE CYCLE DETECTION!
But C is NOT on the current path (D→C) — it was reached via A→B→C earlier.
pathVisited[C] = false (C is finished) → correctly detected as NOT a cycle.
CORRECT RULE:
visited[nbr] = ever been visited (any DFS path)
pathVisited[nbr] = currently on the active DFS stack
CYCLE ←→ pathVisited[nbr] is true when we try to visit nbr
CRITICAL: pathVisited[start] = false AFTER returning (backtrack)
Removing start from the current path when we're done with it.
1import java.util.*;
2
3public class CycleDetectionDFSDirected {
4
5 public static boolean dfs(int start,
6 ArrayList<ArrayList<Integer>> graph,
7 boolean[] visited,
8 boolean[] pathVisited) {
9 visited[start] = true;
10 pathVisited[start] = true; // Mark on current path
11
12 for (int nbr : graph.get(start)) {
13 if (!visited[nbr]) {
14 if (dfs(nbr, graph, visited, pathVisited)) return true;
15 } else if (pathVisited[nbr]) {
16 // nbr is on the CURRENT active DFS path → back edge → CYCLE!
17 return true;
18 }
19 }
20
21 pathVisited[start] = false; // Remove from current path (backtrack)
22 return false;
23 }
24
25 public static void main(String[] args) {
26 Scanner sc = new Scanner(System.in);
27 int V = sc.nextInt();
28 int E = sc.nextInt();
29
30 ArrayList<ArrayList<Integer>> graph = new ArrayList<>();
31 for (int i = 0; i <= V; i++) graph.add(new ArrayList<>());
32
33 for (int i = 0; i < E; i++) {
34 int u = sc.nextInt();
35 int v = sc.nextInt();
36 graph.get(u).add(v); // Directed — only one direction
37 }
38
39 boolean[] visited = new boolean[V + 1];
40 boolean[] pathVisited = new boolean[V + 1];
41 boolean flag = false;
42
43 for (int i = 0; i <= V; i++) {
44 if (!visited[i]) {
45 if (dfs(i, graph, visited, pathVisited)) {
46 flag = true;
47 break;
48 }
49 }
50 }
51
52 System.out.print(flag ? "Cycle detected" : "Cycle not detected");
53 }
54}
55
56/*
57Sample Input (cyclic directed):
583 3
590 1
601 2
612 0
62
63Output: Cycle detected
64
65Sample Input (DAG):
664 4
670 1
680 2
691 3
702 3
71
72Output: Cycle not detected
73*/Cyclic directed (0→1→2→0): Cycle detected DAG (0→1→2→3): Cycle not detected
Dry Run: DFS Directed Graph Cycle Detection
Graph: 0 ──→ 1 ──→ 2
↑ |
└───────┘ Cycle: 1→2→1 (directed!)
Adjacency list (directed):
0: [1]
1: [2]
2: [1] ← creates the cycle
visited = [F, F, F]
pathVisited = [F, F, F]
Outer loop: i=0, not visited
dfs(0):
visited[0]=T, pathVisited[0]=T
nbr=1: not visited
dfs(1):
visited[1]=T, pathVisited[1]=T
nbr=2: not visited
dfs(2):
visited[2]=T, pathVisited[2]=T
nbr=1: visited! AND pathVisited[1]=T → CYCLE DETECTED!
return true
return true
return true
Output: "Cycle detected" ✓
CONTRAST — cross edge (NO cycle):
0 ──→ 1 ──→ 2
↑
3 ──────────┘
dfs(0): visits 0,1,2 → finishes. pathVisited[2]=false after 2 finishes.
dfs(3): visits 3, tries 2.
visited[2]=T but pathVisited[2]=F → NOT a cycle → skip.
Output: "Cycle not detected" ✓
Approach 4: Kahn's Algorithm — Directed Graph
BFS-based approach using in-degrees. If all vertices are processed → no cycle. If some remain unprocessed → cycle.
ALGORITHM (Kahn's):
1. Compute in-degree for every vertex.
2. Enqueue all vertices with in-degree 0.
3. While queue not empty:
Dequeue vertex u.
count++.
For each neighbour v of u: decrement in-degree[v].
If in-degree[v] == 0: enqueue v.
4. If count == V: no cycle (valid topological order).
If count < V: cycle exists (those V-count vertices stuck with in-degree > 0).
INTUITION:
Cycle vertices always have incoming edges from within the cycle.
Their in-degree NEVER reaches 0 while cycle members exist.
They are never enqueued → never counted.
count < V signals cycle.
1import java.util.*;
2
3public class CycleDetectionKahns {
4
5 public static boolean hasCycle(int V, ArrayList<ArrayList<Integer>> graph) {
6 int[] inDegree = new int[V];
7
8 // Compute in-degrees
9 for (int u = 0; u < V; u++) {
10 for (int v : graph.get(u)) {
11 inDegree[v]++;
12 }
13 }
14
15 // Enqueue all vertices with in-degree 0
16 Queue<Integer> queue = new LinkedList<>();
17 for (int i = 0; i < V; i++) {
18 if (inDegree[i] == 0) queue.offer(i);
19 }
20
21 int count = 0;
22 while (!queue.isEmpty()) {
23 int u = queue.poll();
24 count++;
25
26 for (int v : graph.get(u)) {
27 inDegree[v]--;
28 if (inDegree[v] == 0) queue.offer(v);
29 }
30 }
31
32 // If count < V, some vertices were never processed → cycle
33 return count < V;
34 }
35
36 public static void main(String[] args) {
37 Scanner sc = new Scanner(System.in);
38 int V = sc.nextInt(), E = sc.nextInt();
39
40 ArrayList<ArrayList<Integer>> graph = new ArrayList<>();
41 for (int i = 0; i < V; i++) graph.add(new ArrayList<>());
42
43 for (int i = 0; i < E; i++) {
44 int u = sc.nextInt(), v = sc.nextInt();
45 graph.get(u).add(v);
46 }
47
48 System.out.println(hasCycle(V, graph) ? "Cycle detected"
49 : "Cycle not detected");
50 }
51}Cyclic directed: Cycle detected DAG: Cycle not detected
Side-by-Side Comparison
APPROACH GRAPH TYPE DATA STRUCTURE KEY CHECK COMPLEXITY
───────────────────────────────────────────────────────────────────────────────────────
BFS + Pair(node,parent) Undirected Queue nbr != parent O(V + E)
DFS + parent param Undirected Call stack nbr != parent O(V + E)
DFS + pathVisited[] Directed Call stack pathVisited[nbr]==true O(V + E)
Kahn's (in-degree BFS) Directed Queue count < V after BFS O(V + E)
MEMORY:
BFS approaches: O(V) queue + O(V) visited = O(V)
DFS approaches: O(V) visited + O(V) pathVisited + O(h) call stack = O(V)
PREFERENCE:
Undirected: either BFS or DFS — same result, choose based on comfort
Directed: DFS (pathVisited) is more intuitive for interview code
Kahn's doubles as topological sort — versatile
Common Mistakes
Using visited[] alone for directed cycle detection. In directed graphs, a visited node reached via a different DFS path is a cross edge — NOT a cycle. Only a node currently on the ACTIVE recursion path (pathVisited/inStack = true) indicates a back edge and a real cycle. Using visited[] alone gives false positives for diamond-shaped DAGs.
Forgetting pathVisited[start] = false before returning in directed DFS. This is the most common implementation bug. Without resetting, nodes from earlier DFS paths remain "on the stack" — causing false cycle detection in later paths. Always reset pathVisited on backtrack.
Not handling disconnected graphs with the outer loop. BFS/DFS from a single starting vertex only covers one component. The outer loop for i in range(V): if not visited[i]: bfs/dfs(i) is required to check all components.
Confusing parent check for undirected vs directed. For undirected graphs: nbr != parent — skip only the direct parent edge. For directed: no parent check needed — use pathVisited instead. Applying parent check to directed graphs misses real cycles (the parent edge in an undirected sense doesn't exist in directed graphs).
Kahn's: computing in-degree incorrectly by reading adjacency list backwards. In-degree[v] = number of edges pointing TO v. Iterate over each u's adjacency list and increment in-degree of each neighbour (destination). A common mistake is iterating and incrementing the source instead of the destination.
Interview Questions
Q: Why does cycle detection for undirected graphs check nbr != parent but directed graphs check pathVisited[nbr]?
In an undirected graph, every edge appears in both direction adjacency lists. When DFS traverses from u to v, the edge v→u appears in v's list — visiting it doesn't mean a cycle, it's just the same edge traversed backwards. The parent check filters this out. In directed graphs, edges are one-directional — there's no "edge back to parent" issue. Instead, the problem is distinguishing back edges (real cycles) from cross edges (visiting a node reached via an earlier, unrelated path). Only back edges — reaching a node currently on the active recursion stack — indicate cycles.
Q: What is the difference between a cross edge and a back edge in DFS of a directed graph?
A back edge u→v means v is an ancestor of u in the current DFS tree — v is still on the active recursion stack. This creates a cycle (you can go from v down to u then directly back to v). A cross edge u→v means v was already fully explored in a previous DFS call and is no longer on the active stack — v's pathVisited is false. Cross edges don't create cycles. The back edge test is: visited[nbr] && pathVisited[nbr].
Q: When would you prefer Kahn's over DFS for cycle detection in directed graphs?
Kahn's has two advantages: (1) it simultaneously builds a topological ordering — if the graph is a DAG, the processed order IS a valid topological sort, so you get cycle detection + topological sort in one pass; (2) it avoids recursion stack overflow for very deep graphs. DFS-based detection is often simpler to implement and remembers exactly where the cycle is (you can add cycle path tracking). For interview purposes: if the problem also requires topological sort, use Kahn's. If only cycle detection is needed, DFS is slightly simpler.
Summary
Cycle detection has four canonical implementations — two for undirected, two for directed:
Undirected graphs:
- ›BFS + Pair(node, parent) — queue stores both vertex and its parent; cycle if visited neighbour ≠ parent
- ›DFS + parent parameter — recursion carries parent; same check: visited AND neighbour ≠ parent
Directed graphs:
- ›DFS + pathVisited[] — two boolean arrays:
visited[](ever seen) andpathVisited[](currently on stack); cycle ifpathVisited[nbr]is true; MUST resetpathVisited[u] = falseon backtrack - ›Kahn's algorithm — compute in-degrees, BFS from zero-in-degree vertices; cycle exists when processed count < V
All four run in O(V + E) time and O(V) space.
| Undirected | Directed | |
|---|---|---|
| BFS | Pair(node,parent); nbr!=parent check | Kahn's; count < V |
| DFS | Parent parameter; nbr!=parent check | pathVisited[]; back edge check |
In the next topic you will explore Shortest Path Algorithms — Dijkstra's for positive weights and Bellman-Ford for negative weights.
In BFS cycle detection for an UNDIRECTED graph, why do we store the parent alongside each node in the queue?