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
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.
See it move
Purple = popped now, blue = in the queue, green = finished, orange = already visited (skipped).
n = 7, edges = [[0,1],[0,2],[1,3],[2,3],[2,4],[3,5],[4,5],[5,6]], start = 0step 1/9- start
- 0
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.
grid = ["11000","11010","00011","10000"]step 1/13- islands
- 0
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).
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
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.
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.
Rerunning a BFS after every new edge costs O(V + E) each time. Union-Find merges groups in almost O(1) per edge.
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.
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.
Sub-patterns
Same core idea, different rule for βvalidβ. Learn them in this order.
Flood fill / islands
6 questionsFind, count, measure or repaint the connected regions of a grid.
Multi-source BFS
4 questionsMany cells spread at the same speed; find each cell's distance to the nearest source (or when the spread ends).
Grid shortest path
3 questionsFewest steps from a start cell to a target cell (or to any exit) in a grid where every step costs the same.
Implicit graph / state BFS
5 questionsThe nodes aren't given; they are states (codes, words, squares) and you generate each state's neighbours when you pop it.
Graph connectivity
4 questionsGraphs given as edge lists, adjacency lists or matrices: is there a path, how many groups, can everything be reached, copy the graph.
Bipartite / two-colouring
2 questionsCan the nodes be split into two groups so that every edge goes between the groups?
Don't mix these up
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.
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.
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.
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.
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: 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: 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.
// 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: 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.
// 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.
// 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;
}