Home
Day 3 · Week 1

Depth-First Search (DFS)

Level 1 · Fundamentals  ·  2 hrs: ~45 min theory · ~60 min code · ~15 min notes

🧠 Mental Model

DFS goes as deep as possible down one path before backtracking. It's the natural choice when you care about reachability, structure, or paths rather than shortest distance. Where BFS asks "how far?", DFS asks "can I get there, and what's the shape of what I explored?"

The recursion call stack is the traversal stack. That's why DFS is so short to write recursively — the language manages the stack for you. Two moments matter:

⚙️ Mechanics

  1. Mark the current node visited.
  2. For each unvisited neighbor, recurse into it.
  3. (Optional post-order work here, after the loop.)

Recursive vs Iterative

Recursive is cleaner and preferred in interviews unless depth can exceed the stack limit (~10⁴–10⁵ frames in Java). For very deep graphs, convert to an explicit stack. Note: an iterative stack-based DFS visits nodes in a slightly different order than recursion, which is fine for reachability but matters if you rely on strict pre/post ordering.

Path tracking & backtracking

To enumerate all paths (not just reachability), add the node to a path list before recursing and remove it after — that "undo" is backtracking. Here visited marking is often not used (you may revisit a node on a different path).

📊 Complexity

MetricValueWhy
TimeO(V + E)Each node/edge touched once (for plain traversal).
SpaceO(V)Recursion stack depth + visited set.
BFS vs DFS decision: shortest path / fewest steps → BFS. Connectivity, cycle detection, topological order, path enumeration, tree/DAG structure → DFS.

💻 Reference Implementation (Java)

Recursive DFS (grid flood variant) plus the iterative form for reference.

import java.util.*;

/** Recursive DFS on a grid — counts cells in one region. */
public int dfs(int[][] grid, int r, int c, boolean[][] seen) {
    int rows = grid.length, cols = grid[0].length;
    if (r < 0 || r >= rows || c < 0 || c >= cols) return 0; // bounds
    if (seen[r][c] || grid[r][c] == 0) return 0;      // visited or water

    seen[r][c] = true;                                    // pre-order: mark on entry
    int count = 1;
    count += dfs(grid, r+1, c, seen);
    count += dfs(grid, r-1, c, seen);
    count += dfs(grid, r, c+1, seen);
    count += dfs(grid, r, c-1, seen);
    return count;                                       // post-order: aggregate on the way up
}

/** Iterative DFS on an adjacency list — when recursion depth is a risk. */
public void dfsIterative(List<Integer>[] adj, int start) {
    boolean[] seen = new boolean[adj.length];
    Deque<Integer> stack = new ArrayDeque<>();
    stack.push(start);
    while (!stack.isEmpty()) {
        int u = stack.pop();
        if (seen[u]) continue;     // mark on POP here (may be pushed twice)
        seen[u] = true;
        for (int v : adj[u]) if (!seen[v]) stack.push(v);
    }
}

⚠️ Common Pitfalls

Stack overflow on deep graphs: a 10⁵-node line graph will blow the recursion stack. Know the iterative fallback.
Mixing up visited-marking rules: for reachability, mark and never unmark. For path enumeration, you backtrack (add/remove from path) and usually don't keep a global visited set.
Doing post-order work in pre-order position: topological sort breaks if you record a node before its descendants finish.

🎭 Problem Variances — how Big Tech disguises DFS

The tell is reachability, structure, exhaustive enumeration, or "explore a region." Common disguises:

Disguise / phrasingWhat's really askedTwist to handle
"Number of islands / regions / provinces"Flood-fill connected regionsDFS from each unvisited seed; count restarts
"Area / perimeter / shape of a region"Aggregate in post-orderReturn counts up the recursion
"All paths from A to B"Exhaustive enumerationBacktracking: add→recurse→remove; no global visited
"Clone graph / deep copy"DFS with a visited→copy mapMemoize created nodes to handle cycles
"Does a path/target exist?"ReachabilityEarly-return on hit; mark visited to avoid revisits
"Surrounded regions / capture"DFS from borders inwardMark border-connected as safe, flip the rest
"Count connected components"DFS in an outer loop (Day 4)Restart count = component count
"Very deep chain, n up to 10⁵"Recursion-depth trapConvert to iterative explicit-stack DFS
Key distinction: reachability/structure → keep a global visited set. Enumerate all paths → backtrack (undo on the way up) and usually skip the global visited. Post-order is where you aggregate results.

🎯 Problems

LC 200 Number of Islands Flood-fill each unvisited land cell; count how many floods you start. Med
LC 695 Max Area of Island Same flood, but return the post-order aggregated count. Med
LC 797 All Paths From Source to Target Backtracking DFS — add to path, recurse, remove. No global visited. Med