Depth-First Search: Go Deep, Backtrack, and the Three-Colour Cycle Trick
The depth-first search (DFS) algorithm explained: recursion vs an explicit stack, three-colour cycle detection, connected components, flood fill, O(V+E) time complexity, and interview questions.
It’s late. You opened one Wikipedia page to check one fact, clicked a blue link inside it, then a link inside that, and now — forty minutes and eleven tabs deep — you’re reading about the history of a spice you can’t pronounce. When the thread finally dead-ends on something you already read, you don’t start over. You just hit the back button, back, back, until you land on a page with a link you haven’t chased yet — and you dive again.
That midnight spiral has a name, and it’s one of the two ways to walk any graph or tree there is. It’s depth-first search — DFS — and its whole personality is go as deep as you can before you turn around. By the end of this article you and I will have built it both ways it can be built, and gone deep on the two things interviewers actually probe: why recursion and an explicit stack are secretly the same walk, and why detecting a cycle in a directed graph needs not two states but three.
Let’s start nowhere near a computer
Forget links. Picture yourself dropped at the entrance of a hedge maze, tall green walls, no map.
You need a strategy you can follow without thinking, and here’s the simplest one that works: at every junction, take the first path you haven’t tried yet. Walk it. Keep walking, always grabbing the first untried opening, going deeper and deeper into the maze. Do that and one of two things eventually happens — you hit a dead end, or you arrive somewhere you’ve already been.
Either way you’re stuck, so you backtrack: walk back to the most recent junction that still has a path you haven’t explored, and take it. Not the first junction — the most recent one. You unwind exactly as far as you need to find a fresh door, then plunge in again.
Two details make this actually work, and they’re the whole algorithm:
- You chalk-mark every spot you visit. Without marks you’d loop through the same loop of hedges forever. The chalk is memory: "been here, don’t bother."
- You always return to the most recent unfinished junction. That "most recent" is doing quiet, heavy lifting. The trail of junctions behind you is a stack — last one in is the first one you back out to. You never consult the oldest junction until every newer one is exhausted.
That’s depth-first search in three moves: go deep (first untried door), mark as you go (chalk), and backtrack to the newest choice when you’re stuck. Everything below is just those three moves written down carefully.
Where you already meet it
Once you see "dive deep, chalk-mark, backtrack," it’s hiding all over your day:
- The browser back button. Your history is literally a stack; every "back" pops the most recent page. The rabbit hole from the top of this article was DFS, browser and all.
- The paint-bucket tool. Click one pixel and the fill spreads outward through every same-coloured neighbour until it hits an edge of colour. That spreading is a DFS over the grid.
findwalking your folders. It descends into the first subdirectory, all the way down, before it ever looks at the second — depth-first over the file tree.- Solvers and games — mazes, Sudoku, the "number of islands" grid problem. All of them try a choice, dive, and backtrack when it fails.
But the example that best shows how the idea generalises is the one your build tool runs every time you type npm install or mvn package. A project’s modules form a directed graph: "A depends on B" is a one-way arrow. Before it can build anything, the tool has to do two things at once with a single DFS. First, an order: build B before A, always — that’s a topological sort. Second, a veto: if A depends on B which depends on A, there’s no valid order at all, and the build must fail loudly with "circular dependency."
Here’s the twist that makes directed graphs harder than the maze, and it’s worth holding onto. In an undirected maze, "have I been here before?" has one answer. In a directed dependency graph, that innocent question splits in two: "is this a module I’m still in the middle of building — on my current chain right now?" (that’s the fatal circular dependency) versus "is this a finished module I already built on some other branch?" (perfectly fine, modules get shared all the time). One plain chalk-mark can’t tell those apart. That single distinction is the deep idea we’ll build to — the reason directed-cycle detection needs three colours, not one.
What it actually looks like
Let’s pin the picture down before writing a line. Here’s a small graph, and here’s DFS diving from vertex 0 down to a dead end, then climbing back to the last junction with an unopened door.
Follow the numbered order: 0 first, then straight down its first branch to 1, then down its first branch to 3. Dead end. Backtrack to 1, whose other door leads to 4. Dead end again. Backtrack all the way to 0, take its second door to 2, then down to 5. The enter order is 0 · 1 · 3 · 4 · 2 · 5 — deep before wide, every time.
Here’s the mental shift that makes DFS click: it never asks "what’s
nearest?" It asks "how deep can this one path go before I’m forced to turn
around?" That single-minded plunge is the opposite of breadth-first search,
which fans out level by level. Same graph, same O(V + E) cost — completely
different shape of walk.
Let’s build one, step by step
We’ll assemble the full class at the end. For now, one piece at a time. Every version stores the graph as an adjacency list — adj.get(u) hands you the neighbours of vertex u.
Step 1 — the one thing you must remember
The graph is just vertices and their neighbour lists. The only extra state DFS needs is the chalk: a seen array, one boolean per vertex, so you never walk into the same spot twice.
boolean[] seen = new boolean[adj.size()]; // the chalk marks
List<Integer> order = new ArrayList<>(); // the order we entered verticesThat seen array is the entire difference between an algorithm that finishes and one that spins forever. Hold that thought — we’re about to watch it fail on purpose.
Step 2 — recursive DFS: the walk writes itself
The recursion mirrors the maze exactly. Arrive at a vertex, chalk it, record it, then recurse into each unseen neighbour. When every neighbour is done, the call returns — and returning is backtracking.
private static void visit(List<List<Integer>> adj, int u, boolean[] seen, List<Integer> order) {
seen[u] = true; // mark BEFORE you recurse — this is the chalk mark
order.add(u); // record the node on the way IN (pre-order)
for (int v : adj.get(u)) {
if (!seen[v]) {
visit(adj, v, seen, order);
}
}
}The rule: mark the vertex the instant you arrive, before touching its neighbours. The machine’s call stack is quietly playing the role of your trail of junctions — each recursive call is one step deeper, each return is one step back.
Step 3 — the bug: chalk marks placed too late
Watch what happens if you get sloppy about when you chalk. Suppose you mark the vertex only after the neighbour loop instead of before it:
private static void visitBroken(List<List<Integer>> adj, int u, boolean[] seen, List<Integer> order) {
order.add(u);
for (int v : adj.get(u)) {
if (!seen[v]) {
visitBroken(adj, v, seen, order);
}
}
seen[u] = true; // too late!
}Give this a graph with any cycle at all — say 0 → 1 → 0. You enter 0 (unmarked), recurse to 1 (unmarked), and 1 looks back at 0, which is still not marked, so you recurse into 0 again… and again… until the call stack overflows and the program dies. The chalk exists to break exactly this loop, and it only works if you lay it down before you step forward, not after. Mark on arrival, every time.
Step 4 — pre-order or post-order: when do you write it down?
Notice that Step 2 recorded each vertex on the way in — the moment we arrived. That’s pre-order. But there’s a second, equally useful moment: the instant a vertex is finished, after all its descendants are done, right before the call returns. Recording then is post-order.
Same tree, same walk, two different stamps. Read the blue "enter" numbers top-to-bottom and you get pre-order 0 · 1 · 3 · 4 · 2. Read the grey "leave" numbers and you get post-order 3 · 4 · 1 · 2 · 0 — the leaves finish first, the root finishes last, because the root can’t be done until everything beneath it is.
Post-order is not a curiosity — it’s the engine of topological sort. Reverse the order in which vertices finish and you get an ordering where every arrow points forward: exactly the "build B before A" order your package manager needs. A vertex finishes only after all its dependencies do, so reversing puts dependencies first. We’ll cash this in the interview corner.
Recursion and the explicit stack are the same walk
Here’s the first idea worth slowing all the way down for, because interviewers love to ask "now do it without recursion" and watch you flail.
The recursive version never mentions a stack — so where did the stack go? It was the call stack all along. Every time you recursed, the machine pushed a frame remembering where you were and which neighbours you still had to try; every return popped one. Recursion is just DFS with an invisible stack the runtime manages for you. Make that stack visible and you get the iterative version:
public static List<Integer> dfsIterative(List<List<Integer>> adj, int start) {
boolean[] seen = new boolean[adj.size()];
List<Integer> order = new ArrayList<>();
Deque<Integer> stack = new ArrayDeque<>();
stack.push(start);
while (!stack.isEmpty()) {
int u = stack.pop();
if (seen[u]) {
continue; // it was pushed more than once — skip the repeat
}
seen[u] = true;
order.add(u);
List<Integer> nbrs = adj.get(u);
for (int i = nbrs.size() - 1; i >= 0; i--) { // reverse: matches recursion's order
int v = nbrs.get(i);
if (!seen[v]) {
stack.push(v);
}
}
}
return order;
}Three details in that loop are exactly what a strong candidate points out, and each one is a subtlety the recursion hid from you:
- Push the neighbours in reverse. A stack is last-in-first-out, so whichever neighbour you push last comes off first. To visit them left-to-right like the recursion does, you have to push them right-to-left. Get this wrong and your iterative DFS is still correct — it just visits in a mirror-image order, which quietly fails any test that checks the exact sequence.
- A vertex can sit on the stack more than once. Two different vertices might both push
5as a neighbour before5ever gets popped. That’s why we checkseenagain on pop andcontinuepast repeats — the mark-on-arrival still guarantees each vertex is processed exactly once. (Marking on push instead of on pop also works and keeps the stack smaller, but it changes the visit shape slightly; pick one discipline and stay consistent.) - Pre-order is easy this way; post-order is not. In the recursion, post-order was free — it’s just "after the recursive calls return." With an explicit stack you don’t get that moment handed to you; you need a second marker or two stacks to know when a vertex is truly finished. The equivalence is real, but it isn’t effortless in both directions.
So if the two are the same walk, why ever bother with the explicit stack? One word: depth. The call stack is small — a few thousand to a few tens of thousands of frames before it overflows. Hand recursion a pathological graph that’s a single chain a million vertices long and it dies with a StackOverflowError. The explicit stack lives on the heap, which is enormous, so it strolls through a million-deep graph without blinking. The iterative version isn’t about elegance; it’s the one that survives adversarial input.
The line to say out loud: "Recursive DFS uses the call stack; iterative DFS moves that same stack onto the heap so it can go far deeper." Then mention the reverse-push, and you’ve answered the real question behind "do it without recursion" — which is whether you understand that the stack was never optional, only hidden.
Counting connected components
Now the payoffs. The simplest is counting the separate pieces of an undirected graph — how many friend-circles, how many islands, how many isolated networks.
The trick is a loop wrapped around DFS. Walk every vertex; each time you find one that’s still unchalked, you’ve discovered a brand-new piece, so bump the counter and flood the whole thing so you don’t count it again.
public static int countComponents(int n, int[][] edges) {
List<List<Integer>> adj = buildUndirected(n, edges);
boolean[] seen = new boolean[n];
int components = 0;
for (int s = 0; s < n; s++) {
if (!seen[s]) {
components++; // an unseen vertex starts a brand-new piece
floodComponent(adj, s, seen);
}
}
return components;
}That outer loop matters more than it looks. A single DFS only reaches the vertices connected to where it started — so on a disconnected graph, one call leaves whole islands unvisited. Looping over every vertex, and only starting a fresh flood on the unseen ones, is what guarantees you touch all of them. The same seen array is shared across every flood, so the total work is still O(V + E) — each vertex and edge is handled once, no matter how the pieces fall.
There’s a second, traversal-free way to count components and detect undirected cycles: union-find, which merges vertices as edges arrive and answers "same piece?" in near-constant time. Reach for DFS when the graph is already built and you also want paths or orderings; reach for union-find when edges stream in and you only need connectivity.
The hard one: cycles in a directed graph, in three colours
This is the facet worth the most careful reading, because it’s where a single chalk mark stops being enough.
Go back to the dependency graph. We want to know: does it contain a cycle — a chain of arrows that loops back on itself? Your instinct from the maze is a seen set: if DFS ever steps onto an already-seen vertex, that’s a loop. In a directed graph, that instinct is wrong, and it’s wrong in a way that fails silently.
Look at the diagram. Vertex 2 has an edge to 4, and 4 was already visited — but 4 is finished, fully explored on an earlier branch. Reaching it again is no cycle at all; it’s just two paths sharing a destination, the way two modules both depend on the same library. Now look at vertex 2’s other edge, back to 1. Vertex 1 was also already visited — but 1 is still on the current path, an ancestor we haven’t finished yet. That edge closes a loop. Both targets are "seen." Only one is a cycle. A plain boolean cannot tell them apart.
The fix is to give every vertex one of three colours instead of one bit:
- White — never touched.
- Grey — entered but not finished. This is the crucial one: grey means "on the current DFS path, right now, an ancestor of where I’m standing."
- Black — fully explored, all descendants done, popped off the path for good.
A vertex goes white → grey the moment you enter it, and grey → black the moment you finish it. And the cycle test becomes exact: an edge to a grey vertex is a back edge, and a back edge is a cycle. An edge to a black vertex is just a harmless revisit. An edge to a white vertex is a fresh branch to explore.
private static final int WHITE = 0; // never touched
private static final int GREY = 1; // on the current DFS path (being explored)
private static final int BLACK = 2; // fully explored, off the path
private static boolean explore(List<List<Integer>> adj, int u, int[] colour) {
colour[u] = GREY; // u is now on the current path
for (int v : adj.get(u)) {
if (colour[v] == GREY) {
return true; // an edge to a GREY ancestor = a back edge = a cycle
}
if (colour[v] == WHITE && explore(adj, v, colour)) {
return true;
}
// colour[v] == BLACK: already finished by another route — NOT a cycle
}
colour[u] = BLACK; // all descendants done; u leaves the path
return false;
}Sit with why grey is the whole game. Grey is precisely the set of vertices currently sitting on your recursion stack — the chain of junctions from the start down to where you are. If an arrow points at one of them, it points at somewhere you can already reach and will come back to, so following the arrow and then following the path you were already on brings you around in a circle. Black vertices, by contrast, have been fully drained; the path that made them grey has already unwound, so an edge to a black vertex can’t close a loop back onto your current chain.
This is the mistake that fails the round: using one visited boolean for
directed-cycle detection. It can’t distinguish the grey ancestor from the
black bystander, so it flags the harmless 2 → 4 revisit as a cycle and
reports loops that don’t exist. The diamond 0 → 1, 0 → 2, 1 → 3, 2 → 3 has
no cycle, yet a single-bit checker screams that it does. Three states, not
one.
And the contrast with undirected graphs is illuminating, because there you don’t need three colours. In an undirected graph every edge runs both ways, so the instant you step from A to B, B can "see" A — but that’s the same edge you just crossed, not a cycle. So for undirected graphs the rule is simpler: one seen set, plus skip the parent you came from; a seen neighbour that isn’t your parent is the real back edge. Directed graphs can’t use that shortcut, because "the way you came" isn’t symmetric — which is exactly why they need grey to mark the whole current path, not just the previous step.
Flood fill: DFS wearing a paintbrush
One more application, because it shows DFS on a grid instead of an explicit graph — the paint-bucket tool.
There’s no adjacency list here; a cell’s neighbours are simply up, down, left, and right. "Chalking a vertex" becomes "painting the cell," which does double duty — it records the answer and marks the cell visited, so you never repaint it. The recursion stops at the grid’s edge or at any cell of a different colour, exactly the way the maze walk stopped at walls.
private static void fill(int[][] grid, int r, int c, int old, int newColor) {
if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length) {
return; // walked off the grid
}
if (grid[r][c] != old) {
return; // a different colour: a wall, or already painted
}
grid[r][c] = newColor; // paint here — painting IS marking visited
fill(grid, r + 1, c, old, newColor);
fill(grid, r - 1, c, old, newColor);
fill(grid, r, c + 1, old, newColor);
fill(grid, r, c - 1, old, newColor);
}DFS vs BFS: which walk do you want?
The honest alternative to DFS is its sibling, breadth-first search, which fans out level by level using a queue instead of a stack. They cost the same to run; they answer different questions. Let V be vertices and E be edges.
| Question | Reach for | Why |
|---|---|---|
| Just reach everything / is a path there? | either, O(V + E) | both touch every vertex and edge once |
| Shortest path in an unweighted graph | BFS | level-by-level means the first time you reach a node is the shortest way |
| Cycle detection, topological sort | DFS | needs the finish order and the "on the current path" notion |
| Backtracking search (Sudoku, mazes, N-queens) | DFS | try a choice, dive, undo — depth-first is backtracking |
| Fewest hops / a "closest first" flood | BFS | a queue delivers nodes in ring order from the source |
The one trap worth naming: DFS does not find shortest paths. It happily returns a path between two vertices, but it’s whatever the first plunge stumbled onto, often the scenic route. If "shortest" or "fewest hops" is in the question, that’s BFS’s job in an unweighted graph — and Dijkstra’s once the edges carry weights. Use DFS for structure — connectivity, cycles, ordering, exhaustive search — not for distance.
The complete implementation
Everything above, assembled — recursive and iterative DFS, connected components, three-colour directed cycle detection, and flood fill:
package dev.fiveyear.dfs;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
public final class DepthFirstSearch {
private DepthFirstSearch() {}
/** Visit order of a recursive DFS from start over an adjacency list. O(V + E). */
public static List<Integer> dfsRecursive(List<List<Integer>> adj, int start) {
boolean[] seen = new boolean[adj.size()];
List<Integer> order = new ArrayList<>();
visit(adj, start, seen, order);
return order;
}
private static void visit(List<List<Integer>> adj, int u, boolean[] seen, List<Integer> order) {
seen[u] = true; // mark BEFORE you recurse — this is the chalk mark
order.add(u); // pre-order: record the node on the way IN
for (int v : adj.get(u)) {
if (!seen[v]) {
visit(adj, v, seen, order);
}
}
}
/** The same walk, driven by an explicit stack instead of the call stack. O(V + E). */
public static List<Integer> dfsIterative(List<List<Integer>> adj, int start) {
boolean[] seen = new boolean[adj.size()];
List<Integer> order = new ArrayList<>();
Deque<Integer> stack = new ArrayDeque<>();
stack.push(start);
while (!stack.isEmpty()) {
int u = stack.pop();
if (seen[u]) {
continue; // it was pushed more than once — skip the repeat
}
seen[u] = true;
order.add(u);
List<Integer> nbrs = adj.get(u);
for (int i = nbrs.size() - 1; i >= 0; i--) { // reverse: matches recursion's order
int v = nbrs.get(i);
if (!seen[v]) {
stack.push(v);
}
}
}
return order;
}
/** How many connected pieces an undirected graph splits into. O(V + E). */
public static int countComponents(int n, int[][] edges) {
List<List<Integer>> adj = buildUndirected(n, edges);
boolean[] seen = new boolean[n];
int components = 0;
for (int s = 0; s < n; s++) {
if (!seen[s]) {
components++; // an unseen vertex starts a brand-new piece
floodComponent(adj, s, seen);
}
}
return components;
}
private static void floodComponent(List<List<Integer>> adj, int u, boolean[] seen) {
seen[u] = true;
for (int v : adj.get(u)) {
if (!seen[v]) {
floodComponent(adj, v, seen);
}
}
}
private static final int WHITE = 0; // never touched
private static final int GREY = 1; // on the current DFS path (being explored)
private static final int BLACK = 2; // fully explored, off the path
/** True if the directed graph contains a cycle, found via three-colour DFS. O(V + E). */
public static boolean hasDirectedCycle(int n, int[][] edges) {
List<List<Integer>> adj = buildDirected(n, edges);
int[] colour = new int[n]; // every vertex starts WHITE (0)
for (int s = 0; s < n; s++) {
if (colour[s] == WHITE && explore(adj, s, colour)) {
return true;
}
}
return false;
}
private static boolean explore(List<List<Integer>> adj, int u, int[] colour) {
colour[u] = GREY; // u is now on the current path
for (int v : adj.get(u)) {
if (colour[v] == GREY) {
return true; // an edge to a GREY ancestor = a back edge = a cycle
}
if (colour[v] == WHITE && explore(adj, v, colour)) {
return true;
}
// colour[v] == BLACK: already finished by another route — NOT a cycle
}
colour[u] = BLACK; // all descendants done; u leaves the path
return false;
}
/** Paint the 4-connected region containing (r, c) with newColor. */
public static void floodFill(int[][] grid, int r, int c, int newColor) {
int old = grid[r][c];
if (old == newColor) {
return; // already that colour — nothing to flood
}
fill(grid, r, c, old, newColor);
}
private static void fill(int[][] grid, int r, int c, int old, int newColor) {
if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length) {
return; // walked off the grid
}
if (grid[r][c] != old) {
return; // a different colour: a wall, or already painted
}
grid[r][c] = newColor; // paint here — painting IS marking visited
fill(grid, r + 1, c, old, newColor);
fill(grid, r - 1, c, old, newColor);
fill(grid, r, c + 1, old, newColor);
fill(grid, r, c - 1, old, newColor);
}
private static List<List<Integer>> buildDirected(int n, int[][] edges) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) {
adj.add(new ArrayList<>());
}
for (int[] e : edges) {
adj.get(e[0]).add(e[1]);
}
return adj;
}
private static List<List<Integer>> buildUndirected(int n, int[][] edges) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) {
adj.add(new ArrayList<>());
}
for (int[] e : edges) {
adj.get(e[0]).add(e[1]);
adj.get(e[1]).add(e[0]);
}
return adj;
}
}And here it is producing exactly the outputs the comments claim:
List<List<Integer>> adj = new ArrayList<>();
adj.add(new ArrayList<>(List.of(1, 2))); // 0 → 1, 2
adj.add(new ArrayList<>(List.of(3, 4))); // 1 → 3, 4
adj.add(new ArrayList<>(List.of(5))); // 2 → 5
adj.add(new ArrayList<>()); // 3
adj.add(new ArrayList<>()); // 4
adj.add(new ArrayList<>()); // 5
DepthFirstSearch.dfsRecursive(adj, 0); // [0, 1, 3, 4, 2, 5]
DepthFirstSearch.dfsIterative(adj, 0); // [0, 1, 3, 4, 2, 5] — identical
DepthFirstSearch.countComponents(6, new int[][] {{0, 1}, {1, 2}, {3, 4}}); // 3
DepthFirstSearch.countComponents(4, new int[][] {}); // 4 — four islands
DepthFirstSearch.hasDirectedCycle(3, new int[][] {{0, 1}, {1, 2}, {2, 0}}); // true — a triangle
DepthFirstSearch.hasDirectedCycle(4, new int[][] {{0, 1}, {0, 2}, {1, 3}, {2, 3}}); // false — a diamond, not a loopThat diamond returning false is the whole three-colour argument in one line: vertex 3 is reached twice, yet there is no cycle — because the second time, 3 is black, not grey.
The interview corner
DFS is a near-guaranteed interview topic because it’s small to write and rich to probe — the whole test is whether you understand the stack, the marking, and the directed/undirected split. Here’s how to walk in ready.
Ask these before you write a line:
- "Directed or undirected — and do I need to detect cycles, order the vertices, or just reach them?" This decides everything downstream. Directed-cycle detection needs three colours; undirected needs a parent-skip; plain reachability needs neither.
- "How is the graph given — adjacency list, matrix, or an implicit grid?" A grid computes neighbours as up/down/left/right; a matrix costs
O(V)to scan one vertex’s neighbours; a list costsO(degree). Say which you’re assuming. - "Can it be disconnected, and how deep can it get?" Disconnected means you loop DFS over every vertex, not just one start. Very deep — think a million-node chain — means recursion overflows and you reach for the explicit stack.
The follow-up ladder — where a strong candidate keeps climbing:
- "Return the actual path from
AtoB, not just yes/no." Carry aparentmap as you go, or push onto a list on the way down and pop on the way back up; when you reachB, walk the parents backward to rebuild the route. StillO(V + E). - "The graph is a million vertices deep and recursion stack-overflows." Switch to the explicit-stack version — the heap holds far more frames than the call stack’s few thousand. This is the reason the iterative form exists.
- "Detect a cycle in an undirected graph." Drop the three colours: one
seenset plus skip the edge back to your parent. A seen neighbour that isn’t your parent is a genuine back edge. (Or skip traversal entirely with union-find.) - "Order build tasks so every dependency comes first." Topological sort: run DFS, record each vertex in post-order (finish time), then reverse. If you ever step onto a grey vertex, there’s no valid order — the graph has a cycle, and you report the circular dependency.
- "Count strongly connected components in a directed graph." One
seenset isn’t enough; use Tarjan’s low-link in a single DFS, or Kosaraju’s two passes — a DFS to get finish order, then a DFS on the edge-reversed graph in that order. The natural sequel to everything above.
Three mistakes that fail the round:
- Marking visited too late. Chalk the vertex on arrival, before recursing into neighbours. Mark it after — like the broken Step 3 — and any cycle sends you into infinite recursion until the stack blows.
- One boolean for directed-cycle detection. A single
visitedbit can’t tell a grey ancestor (a real cycle) from a black bystander (a harmless shared descendant), so it reports cycles on cycle-free diamonds. Directed cycles need three states. - Forgetting the parent-skip in the undirected case. Without it, every edge instantly looks like a back edge — you always "see" the vertex you just came from — so the checker screams cycle on a plain straight path.
Where to go from here
You now own the core: dive to the first unexplored door, chalk every vertex on arrival, and backtrack to the most recent junction — recursion and an explicit stack are the same walk, and grey means "still on the path." Three natural next stops:
- Breadth-first search — the level-by-level sibling that a queue turns into shortest-paths on unweighted graphs. Same
O(V + E), opposite shape; knowing when to pick which is half of graph interviews. - Topological sort and strongly connected components — post-order reversed gives you build order; Tarjan and Kosaraju layer two DFS passes into the deep structure of directed graphs.
- Backtracking proper — N-queens, Sudoku, word search. Each is a DFS over a tree of choices where "backtrack" means undoing a move, the maze walk applied to problems that have no graph drawn until you draw it. Pair it with the halving intuition in binary search and the amortized "each element touched a constant number of times" accounting from the sliding window, and you’ve got the traversal ideas that carry most rounds.
Next time you surface from a two-hour Wikipedia dive with eleven tabs open, you’ll know exactly what your evening was: a depth-first search, chalk marks and all, backtracking one tab at a time.