Cycle Detection in an Undirected Graph asks a simple-sounding question — does the graph contain a loop? — and it is the cleanest place to learn union-find (disjoint-set union), a structure that answers "are these two things already connected?" almost instantly.
Problem. You are given n nodes labeled 0 to n - 1 and a list of undirected edges. Return
True if the graph contains a cycle, False otherwise.
Example: n = 4, edges = [[0,1], [0,2], [2,3], [1,3]] → True. The path 0 - 1 - 3 - 2 - 0 forms a
loop, so the last edge [1,3] closes a cycle.
The slow way first
You could run DFS from every unvisited node, tracking the parent you came from, and flag a cycle whenever you reach an already-visited node that is not your parent. That works in O(n + e), but it needs you to build an adjacency list and manage a recursion stack and a visited set.
The question to ask: as I add edges one at a time, can I cheaply tell whether the two endpoints are already linked? If they are, the new edge closes a loop. A union-find structure answers exactly that.
The idea: merge groups, watch for a clash
Give every node its own group. Walk the edge list once. For each edge (u, v):
- Find the root (representative) of
uand the root ofv. - If the roots are the same,
uandvare already in one group — adding this edge makes a cycle. ReturnTrue. - If the roots differ, merge the two groups (point one root at the other) and keep going.
If you survive every edge with no clash, there is no cycle.
The key insight: a cycle appears the moment an edge connects two nodes that were already reachable from each other. Union-find tracks reachability as you build the graph.
Walk through it
Step through the animation. The first three edges each join two separate groups, so they merge cleanly and the parent array fills in. When we reach the final edge (1, 3), both endpoints already trace back to root 0 — the edge turns red, and we return True.
Pseudocode
parent[x] = x for every node # each node is its own group
define find(x):
while parent[x] != x: # follow pointers to the root
x = parent[x]
return x
for each edge (u, v):
ru, rv = find(u), find(v)
if ru == rv: # same group already
return True # this edge closes a cycle
parent[rv] = ru # merge the two groups
return False # no cycle foundThe Python solution
def has_cycle(n, edges):
parent = list(range(n))
def find(x):
while parent[x] != x:
x = parent[x]
return x
for u, v in edges:
ru, rv = find(u), find(v)
if ru == rv:
return True
parent[rv] = ru
return Falseparent = list(range(n))makes each node its own root: nodeistarts pointing at itself.find(x)walks the parent pointers up to the root that representsx's group.- For each edge we compute
ruandrv, the roots of both endpoints. - Lines 9 - 10 are the heart of it: if
ru == rv, the endpoints already share a group, so the edge closes a cycle and we returnTrue. - Otherwise
parent[rv] = rumerges the two groups by pointing one root at the other.
Complexity
| Case | Time | Notes |
|---|---|---|
| DFS / BFS | O(n + e) (moderate) | visit every node and edge |
| Union-find (this solution) | O(e * alpha(n)) (moderate) | near-constant per edge |
O(n) (moderate)The find here is plain (no path compression), so a single call can cost O(n) in a degenerate chain; adding path compression and union-by-rank drives the amortized cost to near-constant alpha(n). Either way the space is O(n) for the parent array.
When this pattern shows up
Reach for union-find whenever a problem is about connectivity or grouping: "are these two
in the same component," "how many connected components," "redundant connection," "number of islands"
on a stream of unions. The move is always the same — find two roots, compare, then union.
This counts each undirected edge once. If your input lists every edge in both directions (u to v and v to u), dedupe first — otherwise the second copy of an edge looks like a cycle even when there is none.
Practice
After processing edges (0,1), (0,2), and (2,3) above, every node points back to root 0. What happens when we process the final edge (1,3)?
1. When does union-find conclude an undirected graph has a cycle?
2. What does find(x) return?
3. What does parent[ru] = rv do?
4. What extra space does the solution use?