9.13 Cycle Detection
You need to determine whether a graph contains a cycle.
9.13 Cycle Detection
Problem
You need to determine whether a graph contains a cycle.
Many graph algorithms assume certain structural properties. A build system expects dependency graphs to be acyclic. A workflow engine expects tasks to form a valid execution order. A routing system may need to identify loops. A compiler may need to reject circular imports.
Before applying higher-level algorithms, it is often necessary to answer a simple question:
Does this graph contain a cycle?
The answer frequently determines whether a problem has a valid solution at all.
Solution
Use graph traversal to detect whether a path can return to a previously visited vertex.
The exact technique depends on whether the graph is directed or undirected.
For directed graphs, track traversal state.
For undirected graphs, track parent relationships.
These approaches reflect the different meanings of cycles in the two graph models.
Discussion
A cycle is a path that begins and ends at the same vertex.
Example:
A → B → C → A
or:
A — B — C — A
Cycles are not inherently good or bad.
Their significance depends on the application.
Examples where cycles are expected:
- Road networks
- Communication networks
- Social networks
- State machines
Examples where cycles are often invalid:
- Build dependencies
- Package dependencies
- Task scheduling
- Course prerequisites
Cycle detection therefore appears in both validation and analysis workflows.
Directed Cycles
Consider:
A → B
B → C
C → A
Starting at A:
A
↓
B
↓
C
↓
A
The traversal returns to a vertex already on the current path.
This is the defining characteristic of a directed cycle.
The challenge is distinguishing:
already visited
from:
currently being explored
Those are different situations.
DFS State Tracking
The standard solution uses three states.
UNVISITED
VISITING
VISITED
Meaning:
| State | Meaning |
|---|---|
| UNVISITED | Vertex has never been seen |
| VISITING | Vertex is currently on the DFS path |
| VISITED | Exploration is complete |
Encountering a VISITING vertex indicates a cycle.
Example:
A
↓
B
↓
C
↓
A
When DFS reaches the final A, it discovers a vertex already on the recursion path.
A cycle exists.
Recipe: Directed Cycle Detection
def has_cycle(graph):
state = {}
def dfs(vertex):
if state.get(vertex) == "VISITING":
return True
if state.get(vertex) == "VISITED":
return False
state[vertex] = "VISITING"
for neighbor in graph[vertex]:
if dfs(neighbor):
return True
state[vertex] = "VISITED"
return False
for vertex in graph:
if dfs(vertex):
return True
return False
Example:
graph = {
"A": ["B"],
"B": ["C"],
"C": ["A"],
}
Result:
True
The graph contains a cycle.
Why the Three-State Method Works
Consider:
A → B → C
DFS begins:
VISITING(A)
VISITING(B)
VISITING(C)
Eventually:
VISITED(C)
VISITED(B)
VISITED(A)
No cycle exists.
Now consider:
A → B → C → A
When DFS reaches the final A:
A is VISITING
The traversal has discovered a back edge into the current recursion chain.
This can occur only if a cycle exists.
That observation forms the correctness argument.
Self-Loops
A self-loop is a cycle.
Example:
A → A
The algorithm detects it automatically.
graph = {
"A": ["A"]
}
Execution:
VISITING(A)
neighbor = A
A already VISITING
Cycle detected.
No special handling is required.
Undirected Cycles
Undirected graphs require a different approach.
Consider:
A — B
The adjacency list contains:
{
"A": ["B"],
"B": ["A"]
}
Starting DFS from A:
A
↓
B
The traversal immediately sees:
B → A
but this does not indicate a cycle.
It is simply the edge used to arrive at B.
The algorithm must distinguish:
parent edge
from:
genuine cycle edge
Parent Tracking
The solution is to remember where each DFS call came from.
When exploring:
A → B
the edge:
B → A
is ignored because A is the parent.
Any other previously visited vertex indicates a cycle.
Recipe: Undirected Cycle Detection
def has_cycle(graph):
visited = set()
def dfs(vertex, parent):
visited.add(vertex)
for neighbor in graph[vertex]:
if neighbor == parent:
continue
if neighbor in visited:
return True
if dfs(neighbor, vertex):
return True
return False
for vertex in graph:
if vertex not in visited:
if dfs(vertex, None):
return True
return False
Example:
graph = {
"A": ["B", "C"],
"B": ["A", "C"],
"C": ["A", "B"],
}
Result:
True
The graph contains a cycle.
Understanding Back Edges
In DFS terminology, a back edge points to an ancestor in the DFS tree.
Consider:
A
↓
B
↓
C
↘
A
The edge:
C → A
points upward in the DFS tree.
This is called a back edge.
A directed graph contains a cycle if and only if DFS discovers a back edge.
This characterization appears repeatedly in advanced graph algorithms.
Recipe: Find One Cycle
Sometimes existence is insufficient.
You need the actual cycle.
Store parent pointers during DFS.
def find_cycle(graph):
state = {}
parent = {}
def dfs(vertex):
state[vertex] = "VISITING"
for neighbor in graph[vertex]:
if state.get(neighbor) == "VISITING":
cycle = [neighbor]
current = vertex
while current != neighbor:
cycle.append(current)
current = parent[current]
cycle.append(neighbor)
cycle.reverse()
return cycle
if state.get(neighbor) is None:
parent[neighbor] = vertex
result = dfs(neighbor)
if result:
return result
state[vertex] = "VISITED"
return None
for vertex in graph:
if state.get(vertex) is None:
result = dfs(vertex)
if result:
return result
return None
Possible output:
[
"A",
"B",
"C",
"A"
]
This is useful when reporting dependency errors.
Kahn's Algorithm as Cycle Detection
Topological sorting provides another cycle-detection method.
Recall:
DAG
implies:
topological ordering exists
and:
cycle
implies:
topological ordering does not exist
Therefore:
order = topological_sort(graph)
if len(order) != len(graph):
print("cycle detected")
This is often the preferred technique when dependency validation and ordering are both required.
Union-Find Cycle Detection
For undirected graphs, Union-Find provides another solution.
Process edges one by one.
for u, v in edges:
If:
find(u) == find(v)
then u and v already belong to the same connected component.
Adding the edge creates a cycle.
Otherwise:
union(u, v)
This approach is widely used in:
- Kruskal's algorithm
- Dynamic connectivity
- Network analysis
The full Union-Find data structure is covered in Chapter 11.
Applications
Cycle detection appears in many domains.
Build Systems
Parser
↓
Lexer
↓
Parser
Invalid.
Package Managers
A depends on B
B depends on A
Invalid.
Course Planning
Course A requires B
Course B requires A
Impossible.
Workflow Engines
Validate
↓
Process
↓
Validate
Infinite loop.
Routing Systems
Cycles may indicate routing loops.
Detection helps diagnose network problems.
Complexity Analysis
Let:
V= number of verticesE= number of edges
DFS-based cycle detection:
| Operation | Complexity |
|---|---|
| Directed cycle detection | O(V + E) |
| Undirected cycle detection | O(V + E) |
| Memory usage | O(V) |
Each vertex is processed once.
Each edge is examined a constant number of times.
The algorithm is linear in graph size.
Common Pitfalls
Do not use undirected cycle-detection logic on directed graphs.
The parent rule is insufficient.
Do not confuse previously visited vertices with vertices on the current recursion path.
Only the latter indicates a directed cycle.
Do not forget disconnected components.
A cycle may exist in a region unreachable from the chosen start vertex.
Do not ignore self-loops.
A self-loop is a cycle.
Do not assume topological sorting always succeeds.
Failure often means a cycle exists.
Recipe: Validate a Dependency Graph
A common production workflow is:
if has_cycle(graph):
raise ValueError(
"Circular dependency detected"
)
Only after validation succeeds should scheduling proceed.
order = topological_sort(graph)
Many build systems, workflow engines, package managers, and orchestration platforms follow exactly this pattern.
Takeaway
Cycle detection determines whether a graph contains paths that return to their starting point. In directed graphs, DFS identifies cycles through back edges and recursion-path tracking. In undirected graphs, parent tracking distinguishes genuine cycles from ordinary traversal edges. The algorithm runs in linear time, forms the basis of dependency validation and graph verification, and serves as a prerequisite for topological sorting, scheduling, workflow execution, and many advanced graph algorithms.