A graph is a set of nodes joined by edges. Almost every graph problem is “start somewhere, visit the neighbours, and remember who you've visited so you never go round in circles”. The main choice is the order. Use breadth-first for the fewest steps, and depth-first to explore everything you can reach.
Think of it like this: passing news through friends. You tell your friends, and they tell theirs. Everyone keeps a list of who has already heard, so nobody is told twice. The first round of calls reaches everyone one step away. The second round reaches everyone two steps away. That's breadth-first.
How to spot it
Words and constraints in a problem that point to this pattern:
- Things connected to other things: “friends”, “flights between cities”, “roads”, “links”
- A 2D grid of
'1'/'0'or'#'/'.'where you move up, down, left and right - “Fewest steps” or “shortest path” when every move costs the same: breadth-first
- “How many groups / islands / connected regions?”: start a search from every unvisited node
- “Prerequisites”, “depends on”, “must come before”, “build order”: topological order
Watch it work
Words you'll need:
- A graph has nodes (also called vertices) and edges that connect pairs of nodes. Nodes joined by an edge are neighbours.
- Edges can be two-way, called undirected, like a friendship. They can also be one-way, called directed, like “A must come before B”.
- An adjacency list stores the graph as a map from each node to the list of its neighbours, for example
A → [B, C]. - A grid is a graph in disguise: each cell is a node, and its neighbours are the cells above, below, left and right.
- The visited set records every node you've already reached, so you never process one twice. Without it, a loop such as A → B → D → C → A runs forever.
The trace finds the fewest hops from A to E with breadth-first search (BFS), which uses a queue (first in, first out). The graph's adjacency list is A → [B, C], B → [A, D, E], C → [A, D], D → [B, C], E → [B]. The node being processed is shown in brackets.
The template
Four shapes: build an adjacency list, BFS for fewest steps, DFS to reach everything, and topological order.
Depth-first search (DFS) follows one path as far as it goes before backing up, usually with recursion. It finds a path, but not necessarily the shortest one. Still, it's the shortest code for “visit everything connected to this node”.
A topological order lists nodes so that, for every edge before → after, before comes earlier in the list. Think of putting on socks before shoes.
Kahn's algorithm builds this order using in-degrees. A node's in-degree is how many edges point into it. In other words, it's how many things the node is still waiting for. The steps:
- Start with every node whose in-degree is 0. These nodes have nothing to wait for.
- Each time you place a node, lower the in-degree of the nodes that were waiting for it.
- Any node that reaches 0 is now ready.
Some nodes may never reach 0. Then they're waiting on each other in a loop, called a cycle, and no valid order exists.
// Build an adjacency list from a list of edges.
function buildGraph(edges: [string, string][]): Map<string, string[]> {
const graph = new Map<string, string[]>();
for (const [a, b] of edges) {
if (!graph.has(a)) graph.set(a, []);
if (!graph.has(b)) graph.set(b, []);
graph.get(a)!.push(b); // a → b
graph.get(b)!.push(a); // b → a too, because this graph is undirected (drop for one-way edges)
}
return graph;
}
// BFS: the fewest edges from start to goal, or -1 if goal can't be reached.
function fewestSteps(graph: Map<string, string[]>, start: string, goal: string): number {
const visited = new Set<string>([start]); // mark on ENQUEUE, so nothing is queued twice
let queue: string[] = [start]; // every node at the current distance
let steps = 0;
while (queue.length > 0) {
const next: string[] = []; // every node one step further away
for (const node of queue) {
if (node === goal) return steps; // first time we reach it = fewest steps
for (const nb of graph.get(node) ?? []) {
if (!visited.has(nb)) {
visited.add(nb);
next.push(nb);
}
}
}
queue = next;
steps++;
}
return -1;
}
// DFS: visit everything reachable from node.
function dfs(graph: Map<string, string[]>, node: string, visited: Set<string>): void {
if (visited.has(node)) return; // already explored: stop here, or a cycle loops forever
visited.add(node);
for (const nb of graph.get(node) ?? []) dfs(graph, nb, visited);
}
// Grids: cell [r, c] is a node, and these are the moves to its 4 neighbours.
const DIRS: [number, number][] = [[1, 0], [-1, 0], [0, 1], [0, -1]]; // down, up, right, left
// For each move: const nr = r + dr, nc = c + dc; skip it if off the grid, a wall, or visited.
// Topological order (Kahn's algorithm). An edge [b, a] means "b must come before a".
// Returns [] when there's a cycle, because then no valid order exists.
function topoOrder(nodes: string[], edges: [string, string][]): string[] {
const waitingOn = new Map<string, string[]>(); // b → the nodes that wait for b
const inDegree = new Map<string, number>(); // how many nodes each node still waits for
for (const n of nodes) {
waitingOn.set(n, []);
inDegree.set(n, 0);
}
for (const [b, a] of edges) {
waitingOn.get(b)!.push(a);
inDegree.set(a, inDegree.get(a)! + 1);
}
const ready = nodes.filter((n) => inDegree.get(n) === 0); // nothing to wait for
const order: string[] = [];
for (let i = 0; i < ready.length; i++) { // ready grows as we go; i walks it like a queue
const n = ready[i];
order.push(n);
for (const a of waitingOn.get(n)!) {
inDegree.set(a, inDegree.get(a)! - 1); // one fewer thing to wait for
if (inDegree.get(a) === 0) ready.push(a); // everything a needed is placed
}
}
return order.length === nodes.length ? order : []; // leftovers wait on each other: a cycle
}
Worked example
Shortest path in a grid: the grid holds '.' (open) and '#' (wall). Return the fewest moves from the top-left cell to the bottom-right cell, moving up, down, left or right, or -1 if it can't be reached. For the grid [".", ".", "#"], ["#", ".", "."], [".", ".", "."] the answer is 4.
The first idea: try every possible route with recursion and keep the shortest. The number of routes explodes. An open 10 × 10 grid has tens of thousands of routes that only move right and down. There are far more once routes can turn back. This is too slow.
Notice two things. Every move costs the same, 1, and the question is “fewest moves”. That's exactly what BFS answers. It reaches cells in order of distance, so the first time it reaches the target, that distance is the answer. Each cell is processed at most once.
Name the graph: each cell
[r, c]is a node, and its neighbours are the four cells around it. A neighbour is skipped if it's off the grid, a wall, or already visited. Store visited cells as strings like"1,2", because two different arrays[1, 2]are never equal in aSet.Write BFS one level at a time, counting the levels as steps.
function shortestPath(grid: string[][]): number { const rows = grid.length; const cols = grid[0].length; if (grid[0][0] === "#" || grid[rows - 1][cols - 1] === "#") return -1; const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; const visited = new Set<string>(["0,0"]); let queue: [number, number][] = [[0, 0]]; let steps = 0; while (queue.length > 0) { const next: [number, number][] = []; for (const [r, c] of queue) { if (r === rows - 1 && c === cols - 1) return steps; // reached the target for (const [dr, dc] of dirs) { const nr = r + dr; const nc = c + dc; if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue; // off the grid if (grid[nr][nc] === "#") continue; // a wall const key = nr + "," + nc; if (visited.has(key)) continue; // already queued visited.add(key); next.push([nr, nc]); } } queue = next; steps++; } return -1; // the queue ran dry: the target is walled off }Check it. Step 0 is
[0,0]. Step 1 reaches[0,1]. Step 2 reaches[1,1]. Step 3 reaches[1,2]and[2,1]. Step 4 reaches[2,2], the target. Answer 4. Edge cases: a 1 × 1 open grid returns 0 straight away, and a target sealed off by walls empties the queue and returns-1.
Common mistakes
- Leaving out the visited set: in any graph with a loop the search never ends, and on a grid it bounces between two cells forever.
- Marking a node visited when it leaves the queue instead of when it enters: the same node is queued many times, and on a big grid the queue explodes.
- Storing grid cells in a
Setas arrays:visited.has([1, 2])is alwaysfalse, because two arrays are never the same object. Use a string key liker + ',' + c. - Using DFS for “fewest steps”: depth-first finds a path, often a long one. Only BFS guarantees the shortest when every step costs the same.
- In Kahn's algorithm, forgetting nodes that have no edges at all: they start with in-degree 0 and belong in the ready list. If you miss them, the order comes out too short and looks like a cycle.
Cost
With V nodes and E edges, BFS and DFS add each node to the visited set once. They look along each edge once, or twice when the graph is undirected. That gives O(V + E) time and O(V) space for the visited set and the queue. A grid with R rows and C columns has R × C nodes with up to 4 edges each, so it's O(R × C). Kahn's algorithm is also O(V + E).
In interviews
Start by naming the graph out loud: “each cell is a node, connected to its four neighbours” or “each course is a node, with an edge from a prerequisite to the course that needs it”. Then pick BFS or DFS and give the reason. Graph questions are often disguised as grids, word puzzles or dependency lists, so spotting the graph is half the work.