Cycle Detection

Context: FIT2004_MOC · the W5 applied spine — DFS with a third vertex state, because on a directed graph “already visited” and “on the current path” stop being the same question Parent Framework: Uninformed Search (BFS and DFS)

Quick Revision

  • 🎯 Objective: a cycle exists iff DFS finds an edge back into the branch it is currently inside ➔ upgrade the boolean visited to unvisited / active / inactive.
  • 📦 Core Components: undirected ➔ any edge to a visited non-parent | directed ➔ edge to an active vertex only | shortest cycle ➔ BFS from every source | pure-cycle component ➔ every .
  • ⚡ Key Constraint: an edge to an inactive vertex is a second path to a finished branch, not a cycle — collapsing the two states is the entire failure mode of the undirected algorithm on a digraph.

📝 How It Works

1. Why the Undirected Algorithm Breaks on a Digraph

  • The undirected rule ➔ DFS meets an edge to an already-visited vertex (other than the one it came from) ⟹ report a cycle. Correct, because an undirected edge can be walked both ways, so a second route back is a closed walk.
  • The digraph counterexample ➔ , , , . DFS reaches via , later reaches again via , and the undirected rule fires — but every edge points forward, so this is a DAG.
  • The real question ➔ not have I seen before? but can I get back to where I am standing? Two forward paths to a common descendant are a diamond; only an edge into an ancestor closes a cycle.
  • Discriminating the two ➔ in , , , , , the edge must be ignored (a finished sibling branch) while must fire ( is still on the path) — same “already visited” flag, opposite verdicts.

2. The Three States

  • Unvisited ➔ never reached.
  • Active ➔ entered, descendants still being explored ➔ the vertex sits on the current root-to-here path in the DFS tree.
  • Inactive ➔ entered and finished ➔ its whole subtree is settled and the search has backtracked past it.
  • The test ➔ on edge , status[v] == Active ⟹ back edge ⟹ directed cycle. status[v] == Inactive ⟹ cross/forward edge ⟹ ignore. Unvisited ⟹ recurse.
  • Where the third state is written ➔ status[u] = Active on entry, status[u] = Inactive on exit, after the neighbour loop — the same “do it on finish” beat as Topological Sort’s push.
  • The outer loop is mandatory ➔ a digraph need not be reachable from one vertex; seed DFS at every still-unvisited vertex.

3. The Undirected Case Costs , Not (applied P10)

  • The claim ➔ deciding whether an undirected graph has a cycle is — the bound is independent of .
  • Acyclic branch ➔ an acyclic undirected graph is a forest, so ⟹ ; the algorithm never sees more edges than that.
  • Cyclic branch ➔ the search terminates the instant it examines the first edge to an already-visited vertex. Every edge inspected before that moment (parent edges aside) was a tree edge, and a forest of DFS trees holds at most of them ⟹ at most work in total.
  • The transferable move (LO2) ➔ when an algorithm stops on first success, bound the work done before the stop, not the size of the input it never reads ➔ the same argument shape as the universal-sink elimination.

4. The Shortest Cycle (applied P6)

  • Why one BFS is not enough ➔ BFS visits in distance order from the source, not in cycle-length order. On , , , , , , , , , the -cycle is met at distance , while the -cycle is away and surfaces only at distance .
  • The fix ➔ run a separate BFS from every vertex and keep the minimum. If the source lies on a cycle of length , that BFS closes it at distance — before any longer cycle through the source has a chance to be detected.
  • The implementation simplification ➔ when the search is rooted inside the shortest cycle, the first already-visited vertex it walks into is the source itself, so the inner test is just if v == s: return dist[u] + 1 — no general back-edge machinery needed.
  • Bound ➔ searches at each ⟹ time, auxiliary (the dist array is reinitialised per run).
  • Undirected is harder, not easier ➔ the applied sheet leaves it open: the parent edge must be excluded or every edge reports a phantom -cycle, and a vertex reached from two different tree branches needs care.

5. Counting Components That Are Pure Cycles (applied P7)

  • The structural characterisation ➔ a connected undirected component is exactly a cycle iff every vertex in it has — degree leaves a dangling tail, degree leaves a branch, and both disqualify it.
  • The algorithm ➔ the ordinary connected-components DFS, with each recursive call returning a boolean; is_cycle for a component is the conjunction of deg(u) == 2 over its vertices.
  • Conjunction, not early exit ➔ the traversal must finish the component even after the first bad degree, or the remaining vertices stay unvisited and get counted as a fresh component.
  • Bound ➔ , unchanged from plain component counting — the degree test is per vertex on an adjacency list that stores a length.

⚙️ Core Implementation

🔹 Directed cycle detection — three states

🔹 Shortest cycle — one BFS per source

🔹 Counting pure-cycle components

⚖️ Complexity

TaskTraversalTimeAuxiliary spaceDiscriminator
Undirected, does a cycle exist?DFS — independent of stops at the first non-tree edge; acyclic ⟹
Directed, does a cycle exist?DFS, three states status stackback edge edge into an active vertex
Directed, via Topological SortKahn’semitted ⟹ cycle; no recursion
Shortest cycle, directed BFSone BFS answers only for its own source
Count pure-cycle componentsDFSconjunction of over the component
  • Bounds are adjacency-list bounds ➔ on an adjacency matrix every neighbour scan is ⟹ throughout.
  • The line is the trap ➔ it is the one graph bound in W5 that is not , and the reason is early termination, not a cleverer traversal.

📊 Exam Execution Trace & Applied Exercises

Directed graph , , , , , . Adjacency lists in the order written; DFS seeded at .

Manual Execution Trace

StepAtEdge examinedstatus[v] seenActionActive set after
———enter
Unvisitedrecurse into
Unvisitedrecurse into
Unvisitedrecurse into
— (no out-edges)—finish ⟹ Inactive
— exhausted—finish ⟹ Inactive
Unvisitedrecurse into
Inactiveignore — finished branch
ActiveCYCLE ➔ return True—

Final Extracted Output: cycle . The undirected rule would have fired at step instead and reported the non-cycle — the two steps carry the same visited bit and the opposite verdict, which is precisely what the third state buys.

Applied Exercise

Problem: the graph , , , , , , , , , holds a -cycle and a -cycle. What does one BFS from return, and what does the -source version return?

Final Extracted Output: one BFS from meets the length- cycle first and returns ; running BFS from returns , so the minimum over all sources is the correct answer . Distance from the source and cycle length are different orderings — BFS only sorts by the first.

⚠️ Common Mistakes

  • 💡 Importing the undirected rule into a digraph ➔ two forward paths to a shared descendant are a diamond, not a cycle; the answer needs the active state, not the visited bit.
  • 💡 Quoting for the undirected existence question ➔ the sheet’s P10 wants and the two-branch justification (forest ⟹ · early termination after tree edges).
  • 💡 Answering “shortest cycle” with one traversal ➔ any single-source search orders by distance from that source; only the per-source minimum orders by cycle length.
  • 💡 Testing degree without finishing the component ➔ an early return False in P7’s DFS leaves vertices unvisited and inflates the component count.

🧠 Active Recall