
Cycle Detection in Graphs¶
Determining Whether a Path Loops Back¶
After Dijkstra taught us to care about path cost, cycle detection shifts the question back to graph structure: can a path come back to a node that is already on the current route? That matters in dependency graphs, approval workflows, task schedulers, and package managers, where a loop can make progress impossible.
Why this matters in practice
a project dependency graph with a cycle means no valid build order exists
a workflow engine can get stuck if approvals loop back indefinitely
a data pipeline may never finish if one stage depends on its own downstream output
graph_with_cycle = {
"A": ["B", "C", "D"],
"B": ["E", "F"],
"C": ["G"],
"D": ["H"],
"E": ["G"],
"F": [],
"G": ["B"],
"H": [],
}
graph_without_cycle = {
"A": ["B", "C", "D"],
"B": ["E", "F"],
"C": ["G"],
"D": ["H"],
"E": ["G"],
"F": [],
"G": [],
"H": [],
}
def has_cycle(graph):
visited = set()
path_stack = set()
def dfs(node):
if node in path_stack:
return True
if node in visited:
return False
visited.add(node)
path_stack.add(node)
for neighbor in graph[node]:
if dfs(neighbor):
return True
path_stack.remove(node)
return False
return any(dfs(node) for node in graph if node not in visited)
print("Cyclic graph has cycle:", has_cycle(graph_with_cycle))
print("Acyclic graph has cycle:", has_cycle(graph_without_cycle))
print("Cycle-causing branch from B:", graph_with_cycle["B"], "then", graph_with_cycle["E"], "then", graph_with_cycle["G"])Cyclic graph has cycle: True
Acyclic graph has cycle: False
Cycle-causing branch from B: ['E', 'F'] then ['G'] then ['B']
Core Idea and Visual Intuition¶
Cycle detection asks a structural question: does any directed path return to a node that is still active in the current DFS path? If yes, the graph contains a cycle.
Read the graph visually
starting from
A, several branches end normally at leavesthe branch
B -> E -> G -> Bdoes not end; it returns toBthat return means the traversal is revisiting a node still on the active path, not just a node seen somewhere earlier
The node was seen sometime earlier in the traversal, so we do not need to solve it again from scratch.
The node is on the current unfinished route. Seeing it again means we looped back before closing the branch.
Topological sorting works only for directed acyclic graphs. If a cycle exists, no valid dependency order can be produced.
Worked Example and Practice Lab¶
What to notice
visitedprevents repeated full work on finished subproblemspath_lookupmarks nodes that are active right nowwhen the traversal reaches
Bagain whileBis still active, the loop is confirmed
Expected contrast
Original graph has cycle: True
Acyclic version has cycle: FalseGuided Practice¶
Which situation proves a directed cycle during DFS?¶
Why does `B -> E -> G -> B` count as a cycle?¶
Why is cycle detection a natural bridge to topological sorting?¶
Exercises¶
Remove the edge
G -> Band predict the result before running the code.Add a new edge
H -> C. Decide whether that creates a cycle and explain why.Modify
has_cycleso it returns the first repeated node instead of onlyTrueorFalse.
Hint
Track two states separately: nodes already solved before, and nodes still active in the current unfinished route. The second one is the real cycle signal.
Key Takeaway¶
Cycle detection is about path state, not path cost. A directed cycle appears when DFS returns to a node that is still active on the current branch. That idea sets up the next notebook naturally: if no cycle exists, we can try to produce a valid topological order.