Coding Interview Patterns Module 20 – Graph patterns: traversal
Cycle detection: directed and undirected graphs
Find a loop of cables in a network and a circular reference among spreadsheet cells, and return the cycle itself, with the brute force and the signal.
What you will learn
- Detect a loop in an undirected graph by skipping only the edge the search arrived by
- Detect a cycle in a directed graph by tracking which nodes are on the current path
- Extract the cycle itself by walking parent pointers back from the edge that closes it
- Prove that the search reports a cycle exactly when the graph has one
- Test a cycle finder against the definition of a cycle on every small graph
Before you start
What you’ll learn · Before you start
What you will learn
- Detect a loop in an undirected graph by skipping only the edge the search arrived by
- Detect a cycle in a directed graph by tracking which nodes are on the current path
- Extract the cycle itself by walking parent pointers back from the edge that closes it
- Prove that the search reports a cycle exactly when the graph has one
- Test a cycle finder against the definition of a cycle on every small graph
Before you start
On this page
A cycle is a path that leaves a node and comes back to it without using any edge twice. A depth-first search (DFS) detects one by watching the edges it meets. In an undirected graph, an edge to a node already reached, other than the edge the search just arrived by, closes a loop. In a directed graph only an edge back to a node still on the current path does, and parent pointers then give the cycle itself.
Two problems: a loop of cables and a circular reference
Problem A: a loop in a network. A campus has n routers, numbered 0 to n − 1, joined by two-way cables
(a, b), sometimes more than one between the same pair. A loop leaves a router by one cable and comes back
by others, using no cable twice. Return the routers of one loop in order, or []. Limits: 1 ≤ n ≤ 100,000 and at
most 200,000 cables.
n = 5with(0, 1), (1, 2), (2, 3), (3, 1), (3, 4)gives[1, 2, 3].n = 2with(0, 1), (0, 1)gives[0, 1]: two cables between the same routers form a loop.
Problem B: a circular reference. Spreadsheet cells refer to other cells in their formulas. A circular reference
is a chain of cells, each referring to the next, that leads back to its first cell. Return one as a list of cells, or
[]. Limits: up to 100,000 cells and 200,000 references.
A1 = B1 + C1,B1 = D1 * 2,C1 = D1 + 1andD1 = 7have none, although D1 is reached twice.- Changing D1 to
A1 - 3makesA1, B1, D1a circular reference, andE1 = E1 + 1is one of a single cell.
The first examples of both problems
Text description of the diagram
The upper panel shows problem A's first example: five routers, numbered 0 to 4, drawn as boxes, with a plain line for each cable, because a cable can be used in both directions. Cables join 0 and 1, 1 and 2, 2 and 3, 3 and 1, and 3 and 4. The three cables between routers 1, 2 and 3 are labelled "loop": leaving router 1 along one of them, you can come back to it along the other two without using any cable twice. The cables to 0 and 4 are on no loop.
The lower panel shows problem B's spreadsheet. Each box is a cell with its formula, and an arrow points from a cell to each cell its formula refers to. A1 = B1 + C1 has arrows to B1 and C1. B1 = D1 * 2 and C1 = D1 + 1 each have an arrow to D1. D1 first holds the number 7, so the four arrows form a diamond: two routes lead from A1 to D1, but no route leads back to A1, so there is no circular reference. A dashed arrow from D1 to A1, labelled "after the edit", shows D1 changed to A1 - 3. With it, A1 refers to B1, B1 to D1 and D1 back to A1: a circular reference.
The brute force, and why it fails at these sizes
For problem A, take each cable out in turn and search whether its routers are still connected: if so, the cable is on a loop. For problem B, search from every cell for a way back to it.
"""Both brute forces of the lesson. The work is counted, not timed, so every computer prints the same."""
def reaches(adj, start, goal, skip=-1):
"""A plain search from start; skip is a cable the search may not use. Returns (found, list entries read)."""
seen, stack, reads = {start}, [start], 0
while stack:
u = stack.pop()
if u == goal:
return True, reads
for v, cable in adj[u]:
reads += 1
if cable != skip and v not in seen:
seen.add(v)
stack.append(v)
return False, reads
def loop_cable_slow(n, cables):
"""Takes each cable out in turn; if its two routers stay connected, the cable lies on a loop."""
adj = [[] for _ in range(n)]
for i, (a, b) in enumerate(cables):
adj[a].append((b, i))
adj[b].append((a, i))
total = 0
for i, (a, b) in enumerate(cables):
found, reads = reaches(adj, a, b, skip=i)
total += reads
if found:
return (a, b), total
return None, total
def circular_cell_slow(names, refs):
"""Searches from every cell to see whether its references lead back to it."""
index = {name: i for i, name in enumerate(names)}
adj = [[(index[r], None) for r in refs.get(name, [])] for name in names] # references have no cable number
total = 0
for i, name in enumerate(names):
for v, _ in adj[i]: # can a cell this one refers to lead back here?
found, reads = reaches(adj, v, i)
total += reads + 1
if found:
return name, total
return None, total
print("Problem A: take each cable out and search again")
print("example 1:", loop_cable_slow(5, [(0, 1), (1, 2), (2, 3), (3, 1), (3, 4)])[0], "lies on a loop")
print("example 2:", loop_cable_slow(4, [(0, 1), (1, 2), (1, 3)])[0])
print("example 3:", loop_cable_slow(2, [(0, 1), (0, 1)])[0], "lies on a loop")
print("routers in a line, no loop list entries read")
for n in (500, 1_000, 2_000):
print(f"{n:>26,} {loop_cable_slow(n, [(i, i + 1) for i in range(n - 1)])[1]:>17,}")
print()
print("Problem B: search from every cell for a way back")
sheet = ["A1", "B1", "C1", "D1"]
formulas = {"A1": ["B1", "C1"], "B1": ["D1"], "C1": ["D1"]}
print("example 1:", circular_cell_slow(sheet, formulas)[0])
print("example 2:", circular_cell_slow(sheet, {**formulas, "D1": ["A1"]})[0], "is on a circular reference")
print("example 3:", circular_cell_slow(["E1"], {"E1": ["E1"]})[0], "is on a circular reference")
print("cells in a chain, no loop list entries read")
for n in (500, 1_000, 2_000):
names = [f"A{i + 1}" for i in range(n)]
chain = {names[i]: [names[i + 1]] for i in range(n - 1)}
print(f"{n:>26,} {circular_cell_slow(names, chain)[1]:>17,}")
print("twice the size, four times the reads: about 10^10 for A and 5 x 10^9 for B at 100,000") Output
Problem A: take each cable out and search again
example 1: (1, 2) lies on a loop
example 2: None
example 3: (0, 1) lies on a loop
routers in a line, no loop list entries read
500 249,001
1,000 998,001
2,000 3,996,001
Problem B: search from every cell for a way back
example 1: None
example 2: A1 is on a circular reference
example 3: E1 is on a circular reference
cells in a chain, no loop list entries read
500 124,750
1,000 499,500
2,000 1,999,000
twice the size, four times the reads: about 10^10 for A and 5 x 10^9 for B at 100,000
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute_force.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Both are right but quadratic: with no cycle, twice the size costs four times the reads, about 10¹⁰ and 5 × 10⁹ at 100,000, over a minute of C++ and ten of CPython at the speeds of the constraints lesson. Neither returns the cycle.
What to notice
- One search meets every edge. A single DFS reads each list once, so a cycle check can cost O(n + m).
- Undirected: almost any edge back closes a loop. The exception is the one cable the search arrived by.
- Directed: reached before is not enough. The diamond reaches D1 twice and has no cycle.
Five techniques follow from these signals:
- Three states mark each node as unseen, on the current path or done; an edge to a node on the path closes a cycle.
- Skipping the arrival cable ignores only the cable a search came in by, so parallel cables still form a loop.
- Parent pointers record how each node was reached, and walking them back from the closing edge lists the cycle.
- An explicit stack of nodes, each with the position of its next edge, replays a recursive DFS without recursion.
- Union-find and Kahn’s algorithm, the variants, find loops among cables and cycles among references without a DFS.
Finding a loop of cables, step by step Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Finding a circular reference with three states
- Why an edge to the path closes a cycle, and nothing else does
- Returning the cycle itself
- Dry runs on the spreadsheet and the two cables
- The code in five languages
- How much work it does
- Edge cases and common bugs
- Variants: union-find, Kahn’s algorithm and graphlib
- Follow-up questions
- Exercise: return one cycle of a directed graph
- Return one cycle of a directed graph
- A practice set with sample answers
- Check yourself
- Interview questions
- In a directed graph, why is reaching a node that the search has visited before not enough to report a cycle?
- A spreadsheet of 1,000,000 cells must reject any edit that creates a circular reference and show the cells of the loop. Checking the whole sheet takes 2 seconds. How do you check a single edit in milliseconds, and what does the check cost?
- Your loop check on a router network skips the neighbour it came from. It passes the samples but misses the loop in a hidden test where two routers are joined by two cables. Why, and what is the fix?
- A build system has 50,000 targets and 400,000 dependencies. Each commit adds or removes a few dependencies, and the full cycle check before every build takes 3 seconds. Design a faster check, and say how you would report a cycle that runs through 40 targets.
- Key takeaways
5 runnable examples (C++, Go, Java, JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 24 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- cycle (Dictionary of Algorithms and Data Structures) (National Institute of Standards and Technology (NIST))
- directed acyclic graph (Dictionary of Algorithms and Data Structures) (National Institute of Standards and Technology (NIST))
- depth-first search (Dictionary of Algorithms and Data Structures) (National Institute of Standards and Technology (NIST))
- Depth-First Search and Linear Graph Algorithms (R. E. Tarjan, SIAM Journal on Computing 1(2), pages 146–160) (Society for Industrial and Applied Mathematics (SIAM))
- graphlib: functionality to operate with graph-like structures (The Python Standard Library) (Python Software Foundation)
- sys.getrecursionlimit() (The Python Standard Library) (Python Software Foundation)
- Topological sorting of large networks (A. B. Kahn, Communications of the ACM 5(11), pages 558–562) (Association for Computing Machinery (ACM))
- Efficiency of a Good But Not Linear Set Union Algorithm (R. E. Tarjan, Journal of the ACM 22(2), pages 215–225) (Association for Computing Machinery (ACM))
- List (Java SE 25 and JDK 25 API): getLast() (Oracle)
- Package slices: Reverse (The Go Authors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress