Depth-First Search

intermediate30 min

Going deep first

Depth-first search explores by committing to one direction and following it until it dead-ends, then backing up and trying the next option.

On a tree this is exactly pre-order traversal. On a graph one thing changes: a graph can contain cycles, so you must remember where you have been.

static void dfs(int node, List<List<Integer>> adj, boolean[] visited) {
    visited[node] = true;
    System.out.println("visiting " + node);

    for (int neighbour : adj.get(node)) {
        if (!visited[neighbour]) {
            dfs(neighbour, adj, visited);
        }
    }
}

Mark visited before recursing, not after

visited[node] = true;      // first line of the method
for (...) { ... }

If you mark the node after the loop, a cycle sends the search back into a node still marked unvisited, and it recurses forever.

This single line is the difference between DFS on a graph and DFS on a tree. Trees have no cycles, so they need no visited array. Graphs always do.

Counting connected components

The classic use. Start a search from every node not yet reached; each fresh start is one separate group.

static int countComponents(List<List<Integer>> adj, int n) {
    boolean[] visited = new boolean[n];
    int components = 0;

    for (int i = 0; i < n; i++) {
        if (!visited[i]) {
            components++;
            dfs(i, adj, visited);
        }
    }
    return components;
}

The outer loop matters. Searching only from node 0 finds one group and silently misses every disconnected part.

Grid flood fill

A grid is a graph where cells are nodes and adjacent cells are edges. Counting regions of connected cells is the same algorithm:

static final int[] DR = {-1, 1, 0, 0};
static final int[] DC = {0, 0, -1, 1};

static void fill(char[][] grid, boolean[][] seen, int r, int c) {
    seen[r][c] = true;

    for (int d = 0; d < 4; d++) {
        int nr = r + DR[d];
        int nc = c + DC[d];

        if (nr < 0 || nr >= grid.length) continue;
        if (nc < 0 || nc >= grid[0].length) continue;
        if (seen[nr][nc] || grid[nr][nc] == '#') continue;

        fill(grid, seen, nr, nc);
    }
}

The bounds checks must come before reading grid[nr][nc]. Java stops evaluating at the first continue, so an out-of-range index is never used. Reorder them and you get ArrayIndexOutOfBoundsException.

Counting regions then follows the same shape as components:

int regions = 0;
for (int r = 0; r < grid.length; r++) {
    for (int c = 0; c < grid[0].length; c++) {
        if (grid[r][c] == '.' && !seen[r][c]) {
            regions++;
            fill(grid, seen, r, c);
        }
    }
}

Detecting a cycle

In an undirected graph, a cycle exists if you reach an already-visited node that is not the one you came from:

static boolean hasCycle(int node, int parent,
                        List<List<Integer>> adj, boolean[] visited) {
    visited[node] = true;

    for (int neighbour : adj.get(node)) {
        if (!visited[neighbour]) {
            if (hasCycle(neighbour, node, adj, visited)) return true;
        } else if (neighbour != parent) {
            return true;                  // reached a visited node another way
        }
    }
    return false;
}

Passing the parent is what prevents a false positive — every edge you walk down is immediately walkable back up, and that is not a cycle.

For a directed graph the parent trick does not work. You need three states instead of a boolean: unvisited, currently on the stack, and finished. A cycle is an edge back to a node still on the stack.

static final int UNVISITED = 0, IN_PROGRESS = 1, DONE = 2;

static boolean hasCycleDirected(int node, List<List<Integer>> adj, int[] state) {
    state[node] = IN_PROGRESS;

    for (int next : adj.get(node)) {
        if (state[next] == IN_PROGRESS) return true;      // back edge
        if (state[next] == UNVISITED && hasCycleDirected(next, adj, state)) {
            return true;
        }
    }
    state[node] = DONE;
    return false;
}

A plain boolean[] cannot distinguish “on the current path” from “finished elsewhere”, and would report cycles that do not exist.

Iterative DFS

Recursion depth scales with the graph, so a large graph can overflow the stack. An explicit stack removes the limit:

static void dfsIterative(int start, List<List<Integer>> adj, boolean[] visited) {
    Deque<Integer> stack = new ArrayDeque<>();
    stack.push(start);

    while (!stack.isEmpty()) {
        int node = stack.pop();
        if (visited[node]) continue;      // may have been queued twice
        visited[node] = true;

        for (int neighbour : adj.get(node)) {
            if (!visited[neighbour]) {
                stack.push(neighbour);
            }
        }
    }
}

The if (visited[node]) continue; after popping is needed here. Unlike the recursive version, a node can be pushed several times before it is first popped, so you check on the way out rather than only on the way in.

DFS or BFS

QuestionUse
Is everything connected?Either — DFS is shorter
How many separate groups?Either
Does a path exist between two nodes?Either
Is there a cycle?DFS
What is the shortest path?BFS — DFS gives a path, not the shortest
Explore all possible arrangementsDFS with undo — see backtracking

The shortest-path row is the one that catches people. DFS will happily return a long winding route when a direct one exists. If the question involves “fewest steps”, use breadth-first search.

Common mistakes

  • Marking visited after recursing, causing infinite recursion on a cycle.
  • No visited array at all on a graph.
  • Starting from only one node, missing disconnected parts.
  • Bounds check after array access in grid code.
  • A boolean visited array for directed cycle detection — you need three states.
  • Forgetting the parent in undirected cycle detection, reporting every edge as a cycle.
  • Using DFS for shortest paths.
  • Stack overflow on a large graph — go iterative.

Practice

  1. Build a small undirected graph and print every node reachable from node 0.
  2. Count the connected components of an undirected graph.
  3. Given a grid of . and #, count the regions of connected . cells.
  4. Determine whether an undirected graph contains a cycle.
  5. Rewrite exercise 1 iteratively with an explicit stack and confirm the same set of nodes is reached.
Hints
  1. Adjacency list plus one DFS call. Remember to add edges in both directions.
  2. The counting loop.
  3. Flood fill, counting how many times you start a new fill.
  4. Pass the parent down so the edge you arrived on is not mistaken for a cycle.
  5. The order visited may differ from the recursive version — that is fine, only the set matters.

Next

Related