Detect Cycle in Undirected Graph (DFS)
Detect Cycle in an Undirected Graph is a GFG problem (Medium). You get V vertices numbered 0 … V-1 and a list edges, where each [u, v] joins u and v in both directions. Return true if the graph contains a cycle and false otherwise.
- A cycle is a closed walk that returns to its starting vertex without reusing an edge. Going out along an edge and straight back along the same edge does not count.
- The graph can be disconnected; a cycle in any one component makes the answer
true. - A self-loop, an edge from a vertex to itself, is a cycle on its own.
- This page solves it with depth-first search.
- 1 <= V <= 10⁵
- 0 <= E <= 10⁵
- 0 <= edges[i][0], edges[i][1] < V
Intuition
DFS lays down a tree as it goes: stepping from u into an unvisited v makes u the parent of v. A tree has exactly one route between any two vertices, so a cycle is precisely a second route.
When DFS at u meets a neighbour that is already visited, there are only two cases:
- it is
u's parent: that is the edge DFS just walked down, seen from the other end, because an undirected edge is stored at both ends; - it is anything else: a back edge, a second connection, and therefore a cycle.
That is the whole trick to detect cycle in undirected graph using DFS: carry the parent into each call and excuse exactly that one neighbour.
Any question that boils down to "is this undirected graph a forest?" is cycle detection in undirected graph form: Graph Valid Tree (LeetCode 261) is a cycle check plus a connectivity check, and Redundant Connection (LeetCode 684) asks which edge closes the first cycle. Reach for DFS cycle detection with a parent when you already have an adjacency list; reach for Union-Find when edges arrive one at a time.
Approach
Before reading on: run DFS with only a visited array on the two-vertex graph with the single edge 0–1. What does vertex 1 see when it scans its neighbours? What single extra value per call would stop that from counting as a cycle?
Store every edge at both ends
For each [u, v], append v to adj[u] and u to adj[v]. The edge has no direction, so each end must see the other; this double storage is also why the parent later shows up as a visited neighbour.
Mark the vertex and remember its parent
dfs(u, parent) sets visited[u] = True before scanning anything, so a route that loops back to u finds it already marked. The root gets parent = -1, a value no vertex has, so it excuses nobody.
Classify each neighbour
For each v in adj[u]:
- unvisited: a tree edge, so recurse with
dfs(v, u)and returntrueat once if that call found a cycle; - visited and equal to
parent: the edge you arrived on, so skip it; - visited and not
parent: a back edge, so returntrue.
Return false when the list runs out
If every neighbour was new or the parent, nothing below u closes a loop. Returning false lets the caller carry on with its own next neighbour.
Start from every unvisited vertex
Loop s over all vertices and call dfs(s, -1) for each one still unvisited. One call only covers one component, and a cycle in any component makes the answer true.
Detect Cycle in Undirected Graph (DFS) solution in Python | C++ | Java
-1, a value no real vertex has, so the root never excuses any neighbour. Marking 0 visited now means any later edge back into it is noticed.true without checking anything else.-1, a value no real vertex has, so the root never excuses any neighbour. Marking 0 visited now means any later edge back into it is noticed.false and 1 carries on with its remaining neighbours.false and 1 carries on with its remaining neighbours.false and 0 carries on with its remaining neighbours.false and 0 carries on with its remaining neighbours.false: this whole component is a tree. That does not settle the graph. Vertices it never reached may form a cycle of their own, so the outer loop keeps going.-1, a value no real vertex has, so the root never excuses any neighbour. Marking 0 visited now means any later edge back into it is noticed.false and 1 carries on with its remaining neighbours.false and 0 carries on with its remaining neighbours.false: this whole component is a tree. That does not settle the graph. Vertices it never reached may form a cycle of their own, so the outer loop keeps going.true without checking anything else.Common pitfalls
Counting every visited neighbour as a cycle
for v in adj[u]:
if visited[v]:
return Truefor v in adj[u]:
if not visited[v]:
...
elif v != parent:
return TrueThe neighbour DFS came from is always visited, because the edge is stored at both ends. Without the parent test, a graph that is just 0–1 returns true.
Calling dfs but ignoring what it returns
if not visited[v]:
dfs(v, u)if not visited[v]:
if dfs(v, u):
return TrueThe back edge is usually found several calls deep. If the caller throws that True away, the loop carries on, the top-level call returns False, and a cyclic graph is reported as a forest.
Searching only from vertex 0
return dfs(0, -1)
for s in range(V):
if not visited[s] and dfs(s, -1):
return True
return FalseIn the third example the triangle 3–4–5 is not reachable from 0. A single search sees only the path 0–1–2 and answers false.
Edge cases
A path through 10⁵ vertices nests 10⁵ calls and hits Python's recursion limit. Either raise it with sys.setrecursionlimit, or push (vertex, parent) pairs on an explicit stack instead of recursing; the classification of each neighbour is unchanged.
Complexity
Detect Cycle in Undirected Graph (DFS) FAQ
How do you detect a cycle in an undirected graph using DFS?
- Build an adjacency list with each edge stored at both ends.
dfs(u, parent): markuvisited, then for each neighbourv: recurse if unvisited, skip ifv == parent, otherwise returntrue.- Call
dfs(s, -1)from every unvisited vertex. - O(V + E) time, O(V + E) space.
Why pass the parent in DFS cycle detection?
Because an undirected edge appears in both endpoints' lists. When DFS reaches v from u, v's list contains u, which is already visited. That entry is the edge DFS just used, not a second route, so it must be skipped. The parent is the one visited neighbour that proves nothing.
What is a back edge in an undirected graph?
An edge from the current vertex to an already-visited vertex other than its parent. In an undirected DFS every non-tree edge is a back edge to an ancestor, so the graph has a cycle exactly when DFS finds one.
Can this method detect a cycle in a directed graph?
No. In a directed graph, reaching a visited vertex can be harmless: two separate paths may lead into the same vertex without forming a loop. Directed cycle detection needs a third state that marks vertices still on the current DFS path.