Topics › Graphs › Problems on BFS/DFS
Detect a cycle in an undirected graph
Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given an undirected graph with V vertices numbered 0 to V - 1, as an adjacency list adj where adj.get(u) lists the neighbours of u. Every edge u - v therefore appears twice: v is in adj.get(u) and u is in adj.get(v). Return true if the graph contains a cycle, otherwise false.
A cycle is a walk that starts and ends at the same vertex, uses at least one edge, and never reuses an edge. The graph may be disconnected, and the cycle can sit in any component. As in the GfG version, the graph is simple: no self-loops and no two edges between the same pair of vertices.
Constraints: 1 ≤ V, E ≤ 10^5.
Example 1
V = 5, edges = 0-1, 1-2, 1-4, 2-3, 3-4
answer = true // 1 - 2 - 3 - 4 - 1
Example 2
V = 4, edges = 0-1, 1-2, 2-3
answer = false // a simple path, which is a tree
Example 3
V = 7, edges = 0-1, 1-2, 3-4, 4-5, 5-3
answer = true // the cycle 3-4-5 lives in the second component; vertex 6 is isolated
A useful fact to keep in mind: a connected undirected graph is acyclic exactly when it is a tree, and every tree on k vertices has k - 1 edges. A cycle is an “extra” edge, one that connects two vertices that were already connected some other way. All four approaches below are different ways of catching that extra edge.
Approach 1: Brute force, remove each edge and test connectivity
Intuition. An edge u - v lies on a cycle exactly when u can still reach v after that edge is deleted, because the rest of the cycle is a second route between them. So test every edge separately: pretend it is gone, search from u, and see whether v turns up.
Algorithm.
- For every edge
u - v(only whenu < v, so each undirected edge is tested once): - Run a DFS from
uthat is not allowed to use the direct stepu -> v. - If that search reaches
v, returntrue. - If no edge passes the test, return
false.
static boolean bruteForce(int V, List<List<Integer>> adj) {
for (int u = 0; u < V; u++) {
for (int v : adj.get(u)) {
if (u < v && reachableWithoutEdge(adj, u, v)) return true; // u < v: test each edge once
}
}
return false;
}
static boolean reachableWithoutEdge(List<List<Integer>> adj, int src, int dst) {
boolean[] vis = new boolean[adj.size()];
Deque<Integer> stack = new ArrayDeque<>();
stack.push(src);
vis[src] = true;
while (!stack.isEmpty()) {
int x = stack.pop();
for (int y : adj.get(x)) {
if (x == src && y == dst) continue; // the edge under test is not allowed
if (y == dst) return true;
if (!vis[y]) { vis[y] = true; stack.push(y); }
}
}
return false;
}
Complexity. Time O(E · (V + E)): one full graph search per edge. Space O(V) for the visited array and the stack.
It is correct, but with 10^5 edges it would do around 10^10 steps. The waste is obvious: every search rediscovers the same components from scratch.
Approach 2: Optimal, DFS that remembers the parent
Intuition. Do a single DFS and watch what each edge leads to. When we stand at node and look at a neighbour nb, there are three cases.
nbis unvisited: this is a new tree edge; recurse.nbis the vertex we just came from (parent): this is the same edge we walked down, seen from the other end, because undirected edges are stored in both lists. Ignore it.nbis visited and is not the parent: we already reachednbby some route through the DFS tree, and now we found a second route through this edge. Tree path plus this edge is a cycle.
Why is the third case always a real cycle? The vertices visited so far, together with the tree edges, form a tree. Adding any non-tree edge between two of its vertices creates exactly one cycle. The only non-tree “edge” we must not count is the tree edge back to the parent, which is exactly what the parent check filters out.
Because the graph can have several components, the outer loop starts a fresh DFS from every vertex that is still unvisited.
Algorithm.
- Mark all vertices unvisited.
- For each unvisited vertex
s, calldfsFrom(s, -1); returntrueif it reports a cycle. dfsFrom(node, parent): marknode; for each neighbournb, recurse if unvisited, otherwise returntruewhennb != parent.
static boolean dfs(int V, List<List<Integer>> adj) {
boolean[] vis = new boolean[V];
for (int s = 0; s < V; s++) {
if (!vis[s] && dfsFrom(s, -1, adj, vis)) return true; // one call per component
}
return false;
}
static boolean dfsFrom(int node, int parent, List<List<Integer>> adj, boolean[] vis) {
vis[node] = true;
for (int nb : adj.get(node)) {
if (!vis[nb]) {
if (dfsFrom(nb, node, adj, vis)) return true;
} else if (nb != parent) {
return true; // reached an already visited node by a second route
}
}
return false;
}
Dry run on Example 1, with adj[1] = [0, 2, 4], adj[2] = [1, 3], adj[3] = [2, 4], adj[4] = [1, 3]:
| call | neighbour | state | action |
|---|---|---|---|
| dfsFrom(0, -1) | 1 | unvisited | recurse |
| dfsFrom(1, 0) | 0 | parent | skip |
| dfsFrom(1, 0) | 2 | unvisited | recurse |
| dfsFrom(2, 1) | 1 | parent | skip |
| dfsFrom(2, 1) | 3 | unvisited | recurse |
| dfsFrom(3, 2) | 4 | unvisited | recurse |
| dfsFrom(4, 3) | 1 | visited, parent is 3 | cycle, return true |
The edge 4 - 1 is the extra edge: 1 was already reached through 1 -> 2 -> 3 -> 4.
Complexity. Time O(V + E): each vertex is visited once and each adjacency list is scanned once. Space O(V) for vis plus the recursion stack, which can be V deep on a long path.
Approach 3: The same check with BFS
Intuition. Nothing in the argument above depends on the order of exploration; it only needs a spanning tree and the knowledge of which tree edge brought us to each vertex. BFS gives both if the queue stores {node, parent} pairs. A visited neighbour that is not the parent again means a second route. This version is preferable in Java when the graph can contain very long paths, because it uses a heap-allocated queue instead of the call stack. The BFS form is treated in detail in Cycle Detection in Undirected Graph (bfs).
static boolean bfs(int V, List<List<Integer>> adj) {
boolean[] vis = new boolean[V];
for (int s = 0; s < V; s++) {
if (vis[s]) continue;
Deque<int[]> q = new ArrayDeque<>();
q.offer(new int[]{s, -1});
vis[s] = true;
while (!q.isEmpty()) {
int[] cur = q.poll();
int node = cur[0], parent = cur[1];
for (int nb : adj.get(node)) {
if (!vis[nb]) {
vis[nb] = true;
q.offer(new int[]{nb, node});
} else if (nb != parent) {
return true;
}
}
}
}
return false;
}
Complexity. Time O(V + E), space O(V) for the queue and the visited array.
Approach 4: Union-Find
Intuition. Build the graph one edge at a time and keep track of which vertices are already connected. A disjoint-set structure answers “are u and v already in the same component?” almost in constant time. If an edge arrives whose endpoints are already connected, that edge is precisely the extra edge that closes a cycle. Otherwise merge the two components. Each edge must be processed once, so the loop skips the copy where u > v.
static boolean unionFind(int V, List<List<Integer>> adj) {
int[] root = new int[V];
for (int i = 0; i < V; i++) root[i] = i;
for (int u = 0; u < V; u++) {
for (int v : adj.get(u)) {
if (u > v) continue; // each undirected edge once
int ru = find(root, u), rv = find(root, v);
if (ru == rv) return true; // u and v were already connected
root[ru] = rv;
}
}
return false;
}
static int find(int[] root, int x) {
while (root[x] != x) {
root[x] = root[root[x]]; // path halving keeps the trees shallow
x = root[x];
}
return x;
}
Complexity. Time O(E · α(V)) with full union by rank; with path halving alone, as here, it is O(E log V) in the worst case and close to linear in practice. Space O(V) for the parent array. This approach is handy when edges arrive as a stream, since it never needs the whole adjacency list.
Edge cases and pitfalls
- Disconnected graphs. A single DFS from vertex
0only explores one component. Example 3 hides its cycle in the second component, so every method loops over all start vertices. - The parent edge. Forgetting the
nb != parenttest reports a cycle on every edge, since each edge is seen from both ends. - Parallel edges and self-loops. The parent check treats a second
u - vedge as the edge we came along, and misses that two parallel edges form a cycle of length 2. If the input can contain them, track the parent edge index instead of the parent vertex, and returntrueimmediately for a self-loopu - u. - Recursion depth. A path of
10^5vertices makes the recursive DFS10^5frames deep, which can overflow Java’s default stack. Use BFS, union-find, or an explicit stack in that case. - Counting shortcut. For a quick sanity check: the graph is a forest exactly when
E = V - (number of components). Any more edges mean a cycle.
Related problems
- Cycle Detection in Undirected Graph (bfs) is the BFS twin of this problem.
- Cycle Detection in Directed Graph (DFS) shows why the parent trick fails for directed edges and what replaces it.
- Disjoint Set explains the union-find structure used in Approach 4.
- Graph Valid Tree asks for “connected and acyclic”, which is this check plus a connectivity test.
- Bipartite Graph (DFS) uses the same DFS skeleton to look for odd cycles.