GeeksforGeeks Medium

Detect Cycle in Directed Graph (BFS)

Detect Cycle in a Directed Graph (BFS) is a GFG problem (Medium). You get V vertices numbered 0 … V-1 and a list edges, where edges[i] = [u, v] is a directed edge from u to v. Return true if the graph contains a cycle and false if it does not.

  • A cycle follows edge directions and comes back to its starting vertex.
  • The graph may be disconnected; a cycle anywhere counts.
  • A self-loop u → u is a cycle of length one.

The task is to detect cycle in directed graph using BFS, so the answer must come from a queue rather than recursion. With up to 10⁵ vertices and edges, the check has to be linear in V + E.

Constraints
  • 1 <= V <= 10⁵
  • 0 <= E <= 10⁵
  • Fewer than V vertices emitted proves a cycle
graphbfscyclein-degree
Open on GeeksforGeeks ↗
02

Intuition

Turn the question around: instead of hunting for a cycle, try to list the vertices so every edge points forward. That ordering is a topological sort, and it exists exactly when the graph has no cycle.

A vertex with in-degree 0 has nothing that must come before it, so it can go first; removing it may free its successors in turn. Peeling vertices this way from a queue is Kahn's algorithm.

On a cycle a → b → c → a, every vertex keeps an incoming edge from another cycle vertex, so none can ever go first. The peeling stalls, and the number of removed vertices ends below V. That shortfall is the whole cycle test.

How to spot this pattern

The detect cycle in directed graph BFS method fits when the graph is directed and you need a yes/no on cycles without recursion, or when you also want an ordering: course prerequisites (Course Schedule, LeetCode 207 and 210), build systems, spreadsheet formula dependencies, package installs. If the question is undirected, this method does not apply: every undirected edge counts towards both ends, so in-degrees never drop the same way.

03

Approach

Try it first

Before reading on: in the graph 0 → 1 → 2 → 3 → 1 with an extra edge 3 → 4, vertex 4 is not on any cycle. Will Kahn's algorithm ever remove it? What does that tell you about using the leftover vertices as "the cycle"?

1

Build the adjacency list and in-degrees together

For each edge [u, v], append v to adj[u] and add 1 to indeg[v]. One pass over the edges gives both, and indeg[v] is now the number of vertices that must be removed before v is free.

2

Seed the queue with every in-degree-0 vertex

Scan all V vertices and queue each one whose in-degree is 0, not just vertex 0. In a disconnected graph each component has its own sources, and a source left out would leave its whole downstream unremoved.

3

Peel: pop, count, release successors

Pop a vertex u and add 1 to removed. For each v in adj[u], subtract 1 from indeg[v], since the edge u → v is gone. Push v only when indeg[v] reaches 0, so each vertex enters the queue exactly once.

4

Compare the count with V

When the queue empties, return removed < V. Equal means every vertex was peeled and the removal order is a valid topological order; fewer means some vertices waited forever on each other.

5

Why a short count means a cycle

Every DAG has a vertex with in-degree 0, and removing it leaves a smaller DAG, so without a cycle the peel never stalls. On a cycle, the first vertex to go would need in-degree 0 while its predecessor on the cycle is still present, so no cycle vertex is ever removed.

04

Detect Cycle in Directed Graph (BFS) solution in Python | C++ | Java

▶1from collections import deque
▶2 
▶3 
▶4class Solution:
▶5 def isCyclic(self, V: int, edges: List[List[int]]) -> bool:
▶6 adj = [[] for _ in range(V)]
▶7 indeg = [0] * V
▶8 for u, v in edges:
▶9 adj[u].append(v)
▶10 indeg[v] += 1
▶11 queue = deque(u for u in range(V) if indeg[u] == 0)
▶12 removed = 0
▶13 while queue:
▶14 u = queue.popleft()
▶15 removed += 1
▶16 for v in adj[u]:
▶17 indeg[v] -= 1
▶18 if indeg[v] == 0:
▶19 queue.append(v)
▶20 return removed < V
readypeeled012345in-degree0v00v12v21v32v41v5queueremoved0 of 6count incoming edges
indeg[0, 0, 2, 1, 2, 1]incoming edges per vertex
V66 edges
Count the incoming edges of every vertex. A vertex's in-degree is how many vertices must be removed before it is free to go. Only vertices with in-degree 0 can start the order: here 0 and 1.
readypeeled012345in-degree0v00v12v21v32v41v5queue01frontremoved0 of 6queue every in-degree-0 vertex
queue[0, 1]every in-degree-0 vertex
removed0need 6
Queue every vertex with in-degree 0, not just vertex 0. Each has no prerequisite, so any of them can be removed first; 2 separate sources start the peel.
readypeeled012345in-degree0v00v12v21v32v41v5queue1frontremoved01 of 6pop 0: removed 1 of 6
u0popped from the front
removed1of 6
out-edges1to release
Pop 0 and count it as removed: nothing points into it any more, so it can safely take position 1 in the order. Its 1 outgoing edge now disappears.
readypeeled012345in-degree0v00v11v21v32v41v5queue1frontremoved01 of 6in-degree of 2 drops to 1: wait
edge0 → 2removed with 0
indeg[2]1was 2
queue[1]unchanged
indeg[2] drops to 1. Vertex 2 still has 1 incoming edge from vertices not yet removed, so it has to keep waiting.
readypeeled012345in-degree0v00v11v21v32v41v5queueremoved012 of 6pop 1: removed 2 of 6
u1popped from the front
removed2of 6
out-edges2to release
Pop 1 and count it as removed: nothing points into it any more, so it can safely take position 2 in the order. Its 2 outgoing edges now disappear one by one.
readypeeled012345in-degree0v00v10v21v32v41v5queue2frontremoved012 of 6in-degree of 2 hits 0: push 2
edge1 → 2removed with 1
indeg[2]0was 1
queue[2]2 joins
Removing 1 deletes the edge 1 → 2, and indeg[2] drops to 0: every vertex that had to come before 2 is gone. Push 2 now, exactly once.
readypeeled012345in-degree0v00v10v20v32v41v5queue23frontremoved012 of 6in-degree of 3 hits 0: push 3
edge1 → 3removed with 1
indeg[3]0was 1
queue[2, 3]3 joins
Removing 1 deletes the edge 1 → 3, and indeg[3] drops to 0: every vertex that had to come before 3 is gone. Push 3 now, exactly once.
readypeeled012345in-degree0v00v10v20v32v41v5queue3frontremoved0123 of 6pop 2: removed 3 of 6
u2popped from the front
removed3of 6
out-edges1to release
Pop 2 and count it as removed: nothing points into it any more, so it can safely take position 3 in the order. Its 1 outgoing edge now disappears.
readypeeled012345in-degree0v00v10v20v31v41v5queue3frontremoved0123 of 6in-degree of 4 drops to 1: wait
edge2 → 4removed with 2
indeg[4]1was 2
queue[3]unchanged
indeg[4] drops to 1. Vertex 4 still has 1 incoming edge from vertices not yet removed, so it has to keep waiting.
readypeeled012345in-degree0v00v10v20v31v41v5queueremoved01234 of 6pop 3: removed 4 of 6
u3popped from the front
removed4of 6
out-edges1to release
Pop 3 and count it as removed: nothing points into it any more, so it can safely take position 4 in the order. Its 1 outgoing edge now disappears.
readypeeled012345in-degree0v00v10v20v30v41v5queue4frontremoved01234 of 6in-degree of 4 hits 0: push 4
edge3 → 4removed with 3
indeg[4]0was 1
queue[4]4 joins
Removing 3 deletes the edge 3 → 4, and indeg[4] drops to 0: every vertex that had to come before 4 is gone. Push 4 now, exactly once.
readypeeled012345in-degree0v00v10v20v30v41v5queueremoved012345 of 6pop 4: removed 5 of 6
u4popped from the front
removed5of 6
out-edges1to release
Pop 4 and count it as removed: nothing points into it any more, so it can safely take position 5 in the order. Its 1 outgoing edge now disappears.
readypeeled012345in-degree0v00v10v20v30v40v5queue5frontremoved012345 of 6in-degree of 5 hits 0: push 5
edge4 → 5removed with 4
indeg[5]0was 1
queue[5]5 joins
Removing 4 deletes the edge 4 → 5, and indeg[5] drops to 0: every vertex that had to come before 5 is gone. Push 5 now, exactly once.
readypeeled012345in-degree0v00v10v20v30v40v5queueremoved0123456 of 6pop 5: removed 6 of 6
u5popped from the front
removed6of 6
out-edges0none
Pop 5 and count it as removed: nothing points into it any more, so it can safely take position 6 in the order. It has no outgoing edges, so no other in-degree changes.
readypeeled012345in-degree0v00v10v20v30v40v5queueremoved0123456 of 6all 6 removed → return false
removed6V = 6
resultfalseremoved < V
No cycle. All 6 vertices were removed, and the removal order 0, 1, 2, 3, 4, 5 is a valid topological order: every edge points from an earlier vertex to a later one.
05

Common pitfalls

Reusing the undirected BFS parent check

✗ Wrong
if visited[v] and v != parent[u]:
    return True
✓ Right
indeg[v] -= 1
if indeg[v] == 0:
    queue.append(v)

In a directed graph, reaching an already-visited vertex is normal: 0 → 1, 0 → 2, 1 → 3, 2 → 3 reaches 3 twice with no cycle. The parent trick only works when every edge goes both ways.

Seeding the queue with vertex 0 only

✗ Wrong
queue = deque([0])
✓ Right
queue = deque(u for u in range(V) if indeg[u] == 0)

Vertex 0 may itself have incoming edges, and other components have their own sources. Missing a source leaves its whole downstream unremoved, which is then misreported as a cycle.

Pushing a vertex every time its in-degree drops

✗ Wrong
indeg[v] -= 1
queue.append(v)
✓ Right
indeg[v] -= 1
if indeg[v] == 0:
    queue.append(v)

A vertex with two predecessors would be pushed twice and counted twice, so removed can reach V even when a cycle exists. Push only at the moment the count hits 0.

Returning the leftover vertices as "the cycle"

✗ Wrong
return [u for u in range(V) if indeg[u] > 0]
✓ Right
return removed < V  # yes / no only

Leftovers include every vertex downstream of a cycle, not just the cycle itself. To print the actual cycle, follow remaining edges from a leftover vertex until a vertex repeats.

06

Edge cases

Self-loop u → u

The loop adds 1 to indeg[u] and only u itself could remove it, so u is never peeled and the answer is true.

07

Complexity

Time
O(V + E)
Space
O(V + E)
Each edge is read once to build adj and indeg, and once more when its source is removed. Each vertex enters the queue at most once. The adjacency list takes O(V + E); the queue and indeg take O(V).
08

Cycle detection: which method for which graph

The BFS idea changes completely between directed and undirected graphs, which is where most wrong answers come from.

MethodGraphCycle whenAlso gives
Kahn's algorithm (BFS, in-degrees)Directedfewer than V vertices removeda topological order
DFS with 3 statesDirectedan edge reaches a vertex still on the recursion patha back edge to print the cycle
BFS with parent trackingUndirecteda visited neighbour that is not the parentnothing extra
Union-FindUndirectedan edge joins two vertices already in one setconnected components
09

Detect Cycle in Directed Graph (BFS) FAQ

Is Kahn's algorithm BFS?

Yes in mechanics: it processes vertices from a FIFO queue, level by level of dependency. It does not compute shortest distances like textbook BFS, though, and a stack instead of a queue gives an equally valid topological sort and the same cycle answer.

Should I use BFS or DFS for cycle detection in directed graph problems?

Both are O(V + E). Prefer BFS (Kahn's algorithm) when you also need the order, as in Course Schedule II, or when the graph can be deep enough to overflow the recursion stack. Prefer DFS when you need to report the cycle itself, because the recursion path holds it.