Here's a fact worth its own lesson: in an unweighted graph, breadth-first search finds the shortest path (fewest edges) from the start to every other reachable node.
BFS explores in rings of increasing distance. It visits everything one edge away, then everything two edges away, then three, and so on. The first time it reaches a node, it has done so by the fewest possible edges — there's no shorter way, because every shorter distance was fully explored first.
Depth-first search gives you a path, but not necessarily the shortest — it plunges down one branch and might reach a node by a long, winding route before a short one. For "fewest steps" questions, reach for BFS.
Carry the distance alongside each node in the queue, starting at 0 for the source:
queue = [(start, 0)] // (node, dist) pairs
visited = { start: true }
while queue is not empty:
node, dist = queue.removeFirst()
if node == target:
return dist // first arrival = shortest
for each next in graph[node]:
if not visited[next]:
visited[next] = true
queue.append((next, dist + 1))
The same trick works on grids — distance is the number of cell-steps.
A 2D grid is just a graph in disguise: each cell (r, c) is a node, and its
neighbours are the adjacent cells. 4-directionally (up, down, left, right — no
diagonals) the neighbours of (r, c) are:
(r-1, c) (r+1, c) (r, c-1) (r, c+1)
that is, (r±1, c) and (r, c±1). A common way to generate them in code:
dirs = [(-1, 0), (1, 0), (0, -1), (0, 1)]
for each (dr, dc) in dirs:
nr, nc = r + dr, c + dc
// skip if out of bounds or already visited
"Slow and steady" — ring by ring — is exactly why BFS gives the shortest answer. The next problem puts it to work.