Detect Cycle in a Directed Graph
WHAT IT SAYS
Determine whether a directed graph contains a cycle.
WHAT IT'S REALLY ASKING
"The undirected trick — 'visited node that isn't my parent' — lies here: in a directed graph you can legally re-meet a node from a FINISHED exploration (two roads into the same city, no loop anywhere). The question directedness forces is sharper: is this visited node still ON MY CURRENT PATH? Only an edge back into your own unfinished ancestry closes a loop. Visited needs three states, not two."
Port the undirected detector
O(V + E) time — and WRONG on the most basic diamondRun DFS with a boolean visited array; report a cycle whenever a neighbour is already visited (with or without the parent exemption carried over from the undirected version).
WHERE THE WORK IS WASTED — Not waste — a false conviction. Take edges A→B, A→C, B→D, C→D: no cycle (nothing returns anywhere), but DFS finishing the B-branch marks D visited, then the C-branch meets D and the boolean detector cries cycle. The failure is conceptual: 'visited' conflates two situations that directedness pulls apart. Meeting a node whose exploration is COMPLETE means converging roads — every path onward from it was already exhausted without returning to you, so no loop is possible through it. Meeting a node whose exploration is STILL OPEN — an ancestor on your current stack — means your path has come back to bite its own tail. One bit cannot distinguish 'done' from 'in progress', and the distinction is the entire problem.
A cycle is an edge into your own open ancestry.
Give every vertex one of three states: WHITE (untouched), GRAY (DFS has entered but not yet finished it — it sits on the current recursion stack, an open ancestor), BLACK (entered and fully finished — every path out of it exhausted). The claim: a directed graph has a cycle if and only if DFS ever finds an edge pointing at a GRAY vertex. Forward direction: an edge u→v with v gray means v is an ancestor of u on the current DFS path — the tree path v ⇝ u exists, and the edge u→v closes it into a directed cycle, exhibited on the spot. Backward direction: suppose a cycle exists and let v be the FIRST of its vertices that DFS enters. While v is gray, DFS explores everything reachable from v — which includes the rest of the cycle — so it reaches the cycle's edge pointing back at v while v is still gray. The back edge cannot be missed. Meanwhile black vertices are provably safe to re-meet: when a vertex turns black, everything reachable from it has been searched and no gray ancestor was hit, so no path through it returns to the current stack — the diamond's second arrival at D bounces off harmlessly. Bookkeeping in practice: a recStack boolean set on entry and CLEARED ON EXIT alongside the permanent visited flag — the clearing is the step the broken version lacked, and forgetting it silently degrades three states back to two. Sweep all vertices, launching DFS from each white one; cycles hide in any component. The dual worth knowing because it inverts the perspective: Kahn's algorithm repeatedly removes vertices of in-degree zero (nothing left pointing at them). A DAG always offers such a vertex (follow edges backward — finiteness forces a source unless you loop), so removal consumes everything; a cycle's vertices all wait on each other, in-degrees never reaching zero, and the process stalls with vertices remaining. Cycle ⟺ removal count < V. DFS finds the cycle by walking INTO it; Kahn detects it as the residue that peeling cannot touch — the same theorem ('cycle ⟺ no topological order') proved from both ends, and Kahn hands you the topological order for free when the answer is no-cycle.
DFS with recStack, or Kahn's peeling
O(V + E) time either way; O(V) for the state arrays or the in-degree queueDFS: visited[] and recStack[]; on entry set both, recurse into neighbours — gray (recStack true) neighbour returns cycle, white recurses, black skips — on exit clear recStack. Launch from every white vertex. Kahn: compute in-degrees, queue the zeros, repeatedly pop-and-decrement-neighbours, enqueueing new zeros; processed < V means cycle.
Three Colours: Only Gray Convicts
YOU'LL SEE IT AGAIN WHEN
- The graph is DIRECTED — re-meeting a finished vertex is legal convergence, so the undirected visited-means-cycle logic false-positives on diamonds.
- A cycle is precisely an edge into the CURRENT recursion stack — state must distinguish open ancestry (gray) from completed exploration (black).
- Topological order is wanted, or detection should be iterative/incremental — Kahn's peeling gives cycle-detection and the ordering in one pass.