2Brute2Best
πŸ”₯0

Graph BFS / DFS Pattern: Algorithmic Intuition, Sub-patterns & C++ Guide

Treat grids, words, and states as nodes connected by edges, then explore with BFS (shortest steps) or DFS (reach everything).

6 sub-patterns Β· 24 questions

Remember this Β· Graph BFS / DFS
Trigger
Grid of cells, islands, regions, "minimum steps", transformations between states, connected components.
Think
What is a node, what is an edge, and do I need the shortest path or just reachability?
Key question
How do I mark visited so I never process a node twice?
A

What is this pattern?

A graph is things (nodes) and connections (edges). Many questions hide one: the cells of a grid are nodes and neighbouring cells are edges; lock codes, words or board squares are nodes and "one move" is an edge. Once you see the graph, two tools explore it.

BFS uses a queue and visits nodes in order of distance from the start: everything 1 step away, then 2 steps, and so on. So the first time BFS reaches a node is along a shortest path (when every edge costs the same). DFS uses recursion or a stack and goes as deep as it can before backing up. It reaches the same nodes, just in a different order, and is the simplest way to reach everything connected to a start.

The one rule that separates graphs from trees: a graph has cycles, so you can come back to a node you already saw. Keep a visited mark and set it when you push a node, so nothing is processed twice. Then each node and each edge is handled once: O(V + E), which is O(R Β· C) on a grid.

Dropping ink on wet paper. BFS is the ink spreading: after one second it has reached everything one step away, after two seconds everything two steps away, so the time the ink reaches a spot is its distance. DFS is a person exploring a cave with a ball of string: follow one tunnel to its end, walk back to the last fork, try the next tunnel. Both see the whole cave; only the ink tells you how far each spot is. The chalk mark on each visited wall is the visited set.

B

See it move

Purple = popped now, blue = in the queue, green = finished, orange = already visited (skipped).

BFS on a graph: visited on pushn = 7, edges = [[0,1],[0,2],[1,3],[2,3],[2,4],[3,5],[4,5],[5,6]], start = 0step 1/9
graph
0d=0123456
queue
0
0
β–²front
State
start
0
Push the start, mark it visited

BFS from node 0. Purple = popped now, blue = waiting in the queue (already visited), green = finished, orange = a neighbour skipped because it was already visited. d = number of edges from 0.

Purple = cell being explored, blue = on the stack, green = flooded (letter = island), grey = water.

Flood fill: count the islandsgrid = ["11000","11010","00011","10000"]step 1/13
grid
0
1
2
3
4
0
1
1
0
0
0
1
1
1
0
1
0
2
0
0
0
1
1
3
1
0
0
0
0
State
islands
0
Scan cells row by row

Scan every cell. A '1' that no island owns yet starts a new island; a DFS then floods all '1's connected to it up / down / left / right. Grey = water, green = already flooded (letter = which island).

C

How do I recognise it?

  • A grid of cells with land / water, walls, colours, oranges: "islands", "regions", "connected", "surrounded".

  • "Minimum number of steps / moves / transformations" where every move costs the same β†’ BFS.

  • Many starting points that spread at once: "rotting", "nearest 0", "distance to the nearest gate" β†’ multi-source BFS.

  • States that change one small step at a time: a lock wheel, one letter of a word, a knight move, a dice roll β†’ implicit graph BFS.

  • An edge list or adjacency list and "is there a path", "how many groups", "can you visit every room".

  • "Split into two groups so no edge stays inside a group" β†’ two-colouring (bipartite).

Looks like Graph BFS / DFS, but isn't

Shortest path where edges have different weights (times, costs)
β†’ Shortest Path (Dijkstra)

Plain BFS assumes every edge costs 1. With weights, a path with more edges can be cheaper, so the first arrival is no longer the best.

Tasks with prerequisites, "an order that respects the dependencies"
β†’ Topological Sort

The edges have a direction that means "before", and the answer is an order, not a distance or a group. Kahn's algorithm is a BFS on in-degrees.

Edges arrive one by one and you must answer "connected yet?" after each
β†’ Union-Find

Rerunning a BFS after every new edge costs O(V + E) each time. Union-Find merges groups in almost O(1) per edge.

List every path / word search on a board
β†’ Backtracking

Backtracking un-marks a cell when it backs up, because another path may use it. Graph traversal marks a cell once forever: it only asks whether a cell can be reached, not by how many paths.

D

How would I discover it from scratch?

Don't memorise β€œuse graph bfs / dfs”. Ask these questions in order β€” the pattern falls out.

Name them before writing code. In Number of Islands a node is a land cell and an edge joins two land cells that share a side (not a corner). In Open the Lock a node is a code like "0201" and an edge is one wheel turned by one. In Word Ladder a node is a word and an edge joins words that differ in one letter. If you can say "from here I can move to …", you have a graph.

E

Sub-patterns

Same core idea, different rule for β€œvalid”. Learn them in this order.

F

Don't mix these up

BFS (queue) vs DFS (stack / recursion)

Both visit every reachable node in O(V + E). Only BFS visits them in order of distance, so only BFS answers "minimum steps". For counting or sizing groups either works; DFS is usually shorter to write.

Mark visited when pushed vs Mark visited when popped

Marking on pop lets one node be pushed by each of its neighbours before it is popped once, so the queue can hold copies and the work grows. Marking on push keeps every node in the queue at most once.

Multi-source BFS (all sources at once) vs One BFS per source

With S sources, one BFS each costs O(S Β· R Β· C). Putting them all in the queue at distance 0 gives every cell its distance to the nearest source in one O(R Β· C) pass.

Graph traversal (mark forever) vs Backtracking (mark, then un-mark)

Ask whether the question cares about one way to reach a cell or every path. Reachability and shortest steps β†’ mark once. Listing paths or using each cell at most once per path (Word Search) β†’ un-mark when you back up.

G

Code templates (C++)

Flood fill: count and size islands

Scan every cell; an unvisited land cell starts one DFS. The DFS marks the cell first (here by turning it into water), then recurses into the 4 neighbours and returns the island's size.

flood-fill-template.cpp
// Flood fill: every unvisited land cell starts one DFS; everything it reaches is one island.
int dr[4] = {1, -1, 0, 0}, dc[4] = {0, 0, 1, -1};

int sink(vector<vector<char>>& grid, int r, int c) {
    int R = grid.size(), C = grid[0].size();
    if (r < 0 || c < 0 || r >= R || c >= C || grid[r][c] != '1') return 0;
    grid[r][c] = '0';                             // mark BEFORE exploring, so no cell is counted twice
    int size = 1;
    for (int d = 0; d < 4; d++) size += sink(grid, r + dr[d], c + dc[d]);
    return size;                                  // island size (Max Area); ignore it to just count
}

int countIslands(vector<vector<char>>& grid) {
    int islands = 0;
    for (int r = 0; r < (int)grid.size(); r++)
        for (int c = 0; c < (int)grid[0].size(); c++)
            if (grid[r][c] == '1') { islands++; sink(grid, r, c); }
    return islands;
}

Multi-source BFS: distance to the nearest source

Push every source with distance 0 before the loop. dist == -1 doubles as the visited mark. Rotting Oranges, 01 Matrix, Walls and Gates and Map of Highest Peak are this code with different cell rules.

multi-source-bfs-template.cpp
// Multi-source BFS: every source goes in the queue at distance 0, then all spread together.
vector<vector<int>> nearestSource(const vector<vector<int>>& grid) {   // 1 = source, 0 = open, -1 = wall
    int R = grid.size(), C = grid[0].size();
    vector<vector<int>> dist(R, vector<int>(C, -1));                     // -1 = not reached yet
    queue<pair<int, int>> q;
    for (int r = 0; r < R; r++)
        for (int c = 0; c < C; c++)
            if (grid[r][c] == 1) { dist[r][c] = 0; q.push({r, c}); }
    int dr[4] = {1, -1, 0, 0}, dc[4] = {0, 0, 1, -1};
    while (!q.empty()) {
        auto [r, c] = q.front(); q.pop();
        for (int d = 0; d < 4; d++) {
            int nr = r + dr[d], nc = c + dc[d];
            if (nr < 0 || nc < 0 || nr >= R || nc >= C || grid[nr][nc] == -1 || dist[nr][nc] != -1) continue;
            dist[nr][nc] = dist[r][c] + 1;        // first time reached = nearest source
            q.push({nr, nc});
        }
    }
    return dist;
}

Grid BFS: shortest path from a start

One source, mark on push, return at the first pop of the target. For 8 directions extend dr/dc; for extra state make dist 3-D.

grid-bfs-template.cpp
// Shortest path in an unweighted grid: BFS from the start; the first time we pop the target is the answer.
int shortestPath(const vector<vector<int>>& grid, pair<int, int> start, pair<int, int> target) {  // 0 = open, 1 = wall
    int R = grid.size(), C = grid[0].size();
    if (grid[start.first][start.second] || grid[target.first][target.second]) return -1;
    vector<vector<int>> dist(R, vector<int>(C, -1));
    queue<pair<int, int>> q;
    dist[start.first][start.second] = 0;
    q.push(start);
    int dr[4] = {1, -1, 0, 0}, dc[4] = {0, 0, 1, -1};
    while (!q.empty()) {
        auto [r, c] = q.front(); q.pop();
        if (make_pair(r, c) == target) return dist[r][c];
        for (int d = 0; d < 4; d++) {
            int nr = r + dr[d], nc = c + dc[d];
            if (nr < 0 || nc < 0 || nr >= R || nc >= C || grid[nr][nc] || dist[nr][nc] != -1) continue;
            dist[nr][nc] = dist[r][c] + 1;        // mark when PUSHED, not when popped
            q.push({nr, nc});
        }
    }
    return -1;
}

State BFS: generate neighbours on the fly

No adjacency list: neighbours are built from the state (turn one wheel, change one letter). A hash map is both the visited set and the distance table.

state-bfs-template.cpp
// State BFS: nodes are states (strings here), neighbours are generated on the fly.
int minMoves(const string& start, const string& goal, const unordered_set<string>& blocked) {
    if (blocked.count(start)) return -1;
    unordered_map<string, int> dist{{start, 0}};
    queue<string> q;
    q.push(start);
    while (!q.empty()) {
        string s = q.front(); q.pop();
        if (s == goal) return dist[s];
        for (int i = 0; i < (int)s.size(); i++)
            for (int step : {1, 9}) {                       // one wheel up or down (Open the Lock)
                string t = s;
                t[i] = char('0' + (t[i] - '0' + step) % 10);
                if (blocked.count(t) || dist.count(t)) continue;
                dist[t] = dist[s] + 1;
                q.push(t);
            }
    }
    return -1;
}

Components on an adjacency list

Build the list once (both directions for an undirected edge), then count how many times a new DFS has to start. Iterative DFS avoids deep recursion on 10⁡ nodes.

components-template.cpp
// Connectivity: adjacency list + visited; each new DFS start is a new component.
int countComponents(int n, const vector<vector<int>>& edges) {
    vector<vector<int>> adj(n);
    for (auto& e : edges) { adj[e[0]].push_back(e[1]); adj[e[1]].push_back(e[0]); }  // undirected: both ways
    vector<bool> seen(n, false);
    int components = 0;
    for (int s = 0; s < n; s++) {
        if (seen[s]) continue;
        components++;
        vector<int> stack{s};                    // iterative DFS: no recursion depth limit
        seen[s] = true;
        while (!stack.empty()) {
            int u = stack.back(); stack.pop_back();
            for (int v : adj[u]) if (!seen[v]) { seen[v] = true; stack.push_back(v); }
        }
    }
    return components;
}

Bipartite: two-colour with BFS

Colour the start 0, its neighbours 1, theirs 0. An edge between two nodes of the same colour means an odd cycle: not bipartite. Start from every uncoloured node, because the graph may be in pieces.

bipartite-template.cpp
// Two-colouring: give a node colour 0, its neighbours colour 1, and so on; a same-colour edge = not bipartite.
bool isBipartite(const vector<vector<int>>& adj) {
    int n = adj.size();
    vector<int> color(n, -1);
    for (int s = 0; s < n; s++) {                // every component, not just node 0
        if (color[s] != -1) continue;
        color[s] = 0;
        queue<int> q;
        q.push(s);
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (int v : adj[u]) {
                if (color[v] == -1) { color[v] = 1 - color[u]; q.push(v); }
                else if (color[v] == color[u]) return false;   // an odd cycle
            }
        }
    }
    return true;
}