Algorithm

Undirected Cycle Detection

Graphs Pattern

Undirected Cycle Detection

Given an undirected graph, determine whether it contains a cycle — a route that leaves a vertex, returns to it, and never reuses the same edge twice.

The no-reused-edge condition is what stops the trivial case from counting: stepping from A to B and immediately back along the same edge is not a cycle. A genuine cycle needs at least three distinct vertices in a simple graph.

The graph may be disconnected, so a cycle in any component makes the answer true.

CONSTRAINTS
  • V = number of vertices, E = number of edges
  • Edges are undirected — each connects its two endpoints in both directions
  • The graph may be disconnected
  • Assume a simple graph unless stated otherwise: no self-loops, no repeated edges
  • Runs in O(V + E) time and O(V) space
EXAMPLE 1
Input: 4 nodes, edges 0-1, 1-2, 2-3, 3-1
Output: true
Nodes 1, 2 and 3 form a triangle: you can go 1 to 2 to 3 and back to 1, using three different edges. Node 0 hangs off the side and is not part of it, but one cycle anywhere makes the answer true.
EXAMPLE 2
Input: 4 nodes, edges 0-1, 1-2, 2-3
Output: false
This is a straight chain. Reaching node 3 and turning around only retraces edges already used, which the definition excludes. A connected graph with no cycles is a tree.
EXAMPLE 3
Input: 5 nodes, edges 0-1, 2-3, 3-4, 4-2
Output: true
The cycle sits in nodes 2, 3 and 4, while nodes 0 and 1 form a separate piece with no cycle. A search launched only from node 0 would explore its component, find nothing, and wrongly report false — every component must be checked.
EXAMPLE 4
Input: 3 nodes, no edges
Output: false
With no edges there is nothing to traverse and no cycle. Three isolated vertices form three separate components, each trivially acyclic.
Does going from a node to its neighbour and straight back count as a cycle?
No. Every undirected edge can be crossed both ways, so that would make every single edge a cycle and the answer always true. A cycle must not reuse an edge, which is exactly what the parent-skip in the algorithm enforces.
Can the graph be disconnected?
Yes, so the search must restart from every vertex not yet visited. This is the most common source of wrong answers on this problem — checking only the component containing vertex 0.
Are self-loops and duplicate edges possible?
Ask, because both break the simple parent-skip logic. A self-loop is a cycle by itself. Two separate edges between the same pair also form a cycle, but the parent-skip would dismiss the second one as "the edge I came from" unless you track edge identities rather than vertex identities.
Do I return a boolean or the cycle itself?
A boolean here. If asked for the cycle, keep a parent pointer for each vertex — when the cycle is detected you can walk back up the parent chain from both endpoints to recover it.

A cycle here means a genuine loop: leave a vertex, come back, and never cross the same edge twice. That last clause is doing real work, and understanding why is the entire problem.

The difficulty peculiar to undirected graphs

In an undirected graph, every edge works both ways. Store it as an adjacency list and the edge between A and B literally appears twice — B is in A's list, and A is in B's.

So the naive rule "if I reach a visited vertex, there is a cycle" fires instantly and always. Start at A, mark it, step to B, mark it, and then look at B's neighbours: A is there, and A is visited. Cycle reported — on a graph with a single edge.

That is nonsense, and the definition already told us why: walking A to B to A reuses the same edge. It is not a loop, it is a retraced step. So the algorithm needs to distinguish the edge you just arrived on from any other edge leading somewhere you have been.

The fix, and why it is exactly right

When the search moves from A to B, pass along the fact that it came from A. Then, while examining B's neighbours, skip A once — that is the edge just used.

Now consider what it means to find any other visited neighbour. Suppose the search is at vertex X and sees a visited neighbour Y that is not its parent. Y being visited means the search reached it earlier by some route, and that route did not come through the edge X–Y, since that edge is being examined for the first time now. So there are two distinct routes between X and Y: the earlier one, and the direct edge. Two distinct routes joined together form a loop that uses no edge twice.

That is a complete argument, and it works in both directions — if a cycle exists, the search must eventually enter it and come around to a vertex already visited by a different edge. So this test is exact, not approximate.

python
def has_cycle(V, adj):
    visited = [False] * V

    def visit(u, parent):
        visited[u] = True
        for v in adj[u]:
            if v == parent:
                continue                      # the edge we just crossed
            if visited[v]:
                return True                   # a second route to v: cycle
            if visit(v, u):
                return True
        return False

    for u in range(V):
        if not visited[u] and visit(u, -1):   # -1: the root has no parent
            return True
    return False
Crucial Notethe parent is skipped once, and this is only sound in a simple graph. If two separate edges join A and B, the second one is a genuine cycle, yet v == parent dismisses it. Handling multi-edges means tracking which edge you arrived on rather than which vertex — usually by passing an edge index. Ask whether the graph is simple before relying on the shorter version.
Crucial Notethe loop over all vertices at the bottom is not optional. Undirected graphs are frequently in several pieces, and a cycle in a component you never started from is invisible. Passing -1 as the root's parent works because no vertex has that label, so nothing is skipped at the start of each component.

Cost: every vertex is entered once and every edge examined twice, once from each end, so O(V + E) time with O(V) for the visited array and the recursion stack.

Contrast with the directed case

It is worth setting the two side by side, because the rules look superficially similar and are not.

In a directed graph, reaching a visited vertex proves nothing — several forward routes may lead to the same place with no loop involved. There, a cycle requires reaching a vertex that is still in progress on the current path, which needs three states.

In an undirected graph, reaching a visited vertex that is not your parent is a cycle, and two states suffice. The reason for the difference is that undirected edges are traversable both ways, so two routes to a vertex can always be joined into a loop by walking one of them backwards. In a directed graph you cannot walk backwards, so two routes stay two routes.

Same word, different structure, different algorithm. Confusing them is a classic interview error, so being able to state the distinction is worth as much as the code.

Worked example

Edges 0-1, 1-2, 2-3, 3-1.

Adjacency: node 0 has [1]; node 1 has [0, 2, 3]; node 2 has [1, 3]; node 3 has [2, 1].

- visit(0, -1). Mark 0. Its only neighbour is 1, which is unvisited, so recurse with parent 0.
- visit(1, 0). Mark 1. Neighbours: 0 is the parent, skip. 2 is unvisited, recurse with parent 1.
- visit(2, 1). Mark 2. Neighbours: 1 is the parent, skip. 3 is unvisited, recurse with parent 2.
- visit(3, 2). Mark 3. Neighbours: 2 is the parent, skip. Then 1 — which is visited and is not the parent of 3. Two distinct routes to node 1 now exist: the original 0 → 1, and the route 1 → 2 → 3 → 1 just walked. Return true.

The detected cycle is 1-2-3-1, using the three edges 1-2, 2-3 and 3-1, none repeated.

Now the chain 0-1, 1-2, 2-3, which has no cycle. The search walks 0 → 1 → 2 → 3, and at each step the only visited neighbour is the parent, which is skipped. Node 3 has just the one neighbour 2, its parent, so the recursion unwinds returning false all the way. The outer loop finds every vertex visited and returns false.

The counting shortcut, and the incremental alternative

Two facts are worth carrying away.

The first is a structural one. A tree — connected and acyclic — on V vertices has exactly V-1 edges. Fewer than V-1 edges and the graph cannot be connected; more than V-1 and it cannot be acyclic. So on a graph promised to be connected, counting edges answers the question outright: E ≥ V means a cycle exists. That will not tell you where, and it fails on disconnected graphs, where a component can carry a cycle while the total edge count stays low. But it is the reasoning behind Redundant Connection, where exactly V edges on V vertices guarantee exactly one cycle.

The second is a different algorithm entirely. When the input is a list of edges rather than an adjacency structure, union-find is more natural than traversal. Process edges one at a time, maintaining which vertices are currently in the same connected group. For an edge joining u and v, ask whether they are already in the same group — if so, they were already connected by some earlier route, and this edge closes a loop.

python
def has_cycle_dsu(V, edges):
    parent = list(range(V))

    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]     # path halving: flattens as it walks
            x = parent[x]
        return x

    for u, v in edges:
        ru, rv = find(u), find(v)
        if ru == rv:
            return True                       # already connected: this edge closes a cycle
        parent[ru] = rv
    return False

It costs O(E · α(V)), where α is the inverse Ackermann function — under 5 for any input that fits in memory, so effectively constant per edge. Its real advantage is that it works incrementally: edges can arrive over time and each query is answered immediately, where a traversal would have to rerun from scratch. That property is what Redundant Connection and Number of Provinces are built on.

Train the reflex: in an undirected graph, a cycle is just a second way to reach somewhere you have already been. Whether you detect it by skipping the edge you came in on, or by noticing that two vertices were already connected before you joined them, is a matter of what form the input arrives in.

Interactive Strategy Visualization
CYCLE DETECTION (DFS)
AACTIVEBCD
Step Explanation

Start DFS at Node A. Since it's the root, it has no parent (-1).

Call Stack State
Current Node:A
Parent Node:-1
Expert Insight

Undirected edges go both ways. By tracking the parent, we distinguish "going back" from "closing a cycle."

DFS (u, parent) Pattern1 / 4 Phases
O(V × E) Reachability Test Per Edge
O(V + E) DFS With Parent Skipping
O(E · α(V)) Union-Find Incremental Merging