Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 35 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free intro, Pro: 7 of 35 minutes free
Free: Two problems: a loop of cables and a circular reference · The brute force, and why it fails at these sizes · What to notice. With a pass: the other 14 sections and 3 interview answers, about 28 minutes.

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 = 5 with (0, 1), (1, 2), (2, 3), (3, 1), (3, 4) gives [1, 2, 3].
  • n = 2 with (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 + 1 and D1 = 7 have none, although D1 is reached twice.
  • Changing D1 to A1 - 3 makes A1, B1, D1 a circular reference, and E1 = E1 + 1 is one of a single cell.
Five routers whose cables form one loop through routers 1, 2 and 3, and four spreadsheet cells whose references form a diamond.Problem A: cables run both waysProblem B: an arrow means 'refers to'01234A1 = B1 + C1B1 = D1 * 2C1 = D1 + 1D1 = 7, then edited to A1 - 3looplooploopafter the edit

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, with the work counted Python · brute/brute_force.py
"""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

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.

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.