Shortest Path in a Grid (BFS)

Find the fewest moves between two cells of a grid with walls, return the path itself, then handle the two twists candidates report most: a battery that charging cells refill, and a grid with no edges.

Also known as: Shortest path in a binary matrix, Maze shortest path, Shortest path in an infinite grid

Why practise this

BFS shortest path (grids and state graphs) shows up in 12.6% of the 1,447 coding interview reports we studied, at 12 companies. Find the fewest steps across a grid or between states, often with a twist: several movement types, obstacles that change, or a state that has to carry extra information.

Reported at: OpenAI, Snowflake, Databricks, Robinhood, Amazon, Cursor, Zip, Citadel, Waymo, Roblox, Perplexity, xAI, Anthropic.

This is an original practice problem written by AceOffer to train the pattern. It is not a question any company asked.

What candidates report interviewers add

Follow-ups candidates reported on questions with this pattern, in our words. Parts marked “from reports” below are built on them.

The input grid is a non-empty list of strings, all the same length. Each character is one cell: . is open, # is a wall, S is the start and E is the end. There is exactly one S and one E, and both are open cells. Each move steps to one of the four side neighbours (no diagonals) and never leaves the grid.

S..#
.#..
...E

In this grid the shortest path takes 5 moves.

Questions to ask before you code

  • Are diagonal moves allowed? (No, only up, down, left and right.)
  • Should I return the path itself or only its length, and is any shortest path fine?
  • In part 3, does moving onto a charger use a unit first, and can the charge ever exceed capacity?
  • In part 4, is the grid really unbounded, and what should happen when the end is enclosed?
  • If S and E are disconnected, do you want -1, None, or an exception?

Part 1: Path length

Write shortest_path_length(grid) -> int that returns the fewest moves from S to E that never enter a # cell, or -1 when E cannot be reached.

shortest_path_length(["S..#", ".#..", "...E"])  # 5
shortest_path_length(["S#E"])                    # -1

Part 2: Return the pathfrom reports

Write shortest_path(grid) -> list[tuple[int, int]] that returns the cells of a shortest path as (row, col) tuples, starting with the S cell and ending with the E cell. Its length is shortest_path_length(grid) + 1. Return [] when there is no path.

Tie-break: when several shortest paths exist, return the one that is smallest when the lists are compared with Python's < (first differing cell decides, and cells compare by row, then column).

shortest_path(["S..", "...", "..E"])
# [(0, 0), (0, 1), (0, 2), (1, 2), (2, 2)]

Part 3: Limited batteryfrom reports

Grid cells may now also be C, a charging cell (open, like .). You get an integer capacity >= 0.

  • You start at S with capacity units of charge. Every move uses 1 unit, and a move is only allowed when you have at least 1 unit left.
  • After moving onto a C cell, the charge goes back to capacity. S, E and . do not change it.
  • Cells may be revisited.

Write shortest_path_with_battery(grid, capacity) -> int returning the fewest moves from S to E under these rules, or -1 if E cannot be reached.

shortest_path_with_battery(["S.C.E"], 2)  # 4  (recharge on C after 2 moves)
shortest_path_with_battery(["S...E"], 3)  # -1 (needs 4 units)

Part 4: No grid edgesfrom reports

Now the grid is infinite. Points are (x, y) integer tuples, a move changes x or y by 1, and blocked is a set of points you cannot enter. Everything not in blocked is open.

Write shortest_path_unbounded(start, end, blocked) -> int returning the fewest moves from start to end, or -1 if no path exists (for example when end is walled in, or when start or end is itself blocked). start == end returns 0. The function must always finish, even when there is no path.

shortest_path_unbounded((0, 0), (3, 0), set())                        # 3
shortest_path_unbounded((0, 0), (5, 5), {(4, 5), (6, 5), (5, 4), (5, 6)})  # -1

Open this page on a laptop or desktop to write and run your code against the tests in the browser.

Approach

Spoilers: try each part before reading its walkthrough.

Part 1: Path length

Treat each open cell as a node with up to four edges to its side neighbours. Every edge has the same cost, so breadth-first search is enough: it expands cells in rings of increasing distance from S, which means the first time a cell is reached, that distance is already the minimum. The reference keeps a dict steps from cell to distance and a frontier list for the current ring; building the next ring from the current one avoids a deque entirely. A cell enters steps the moment it is discovered, so it is never queued twice. The answer is steps.get(E, -1). Each cell is processed once and checks four neighbours, so time and memory are O(R x C) for an R by C grid.

Part 2: Return the path

Knowing the length is not enough to rebuild the route under a fixed tie-break. The clean trick is to run the same BFS from E instead, labelling every reachable cell with its distance to the end. From S, any neighbour whose label is exactly one smaller lies on some shortest path. Python compares lists of tuples cell by cell, so to get the smallest path you only need the smallest valid next cell at each step. The reference lists neighbours in the order up, left, right, down, which is already ascending (row, col) order, and takes the first that qualifies. That greedy walk can never strand you, because a cell with label d always has a neighbour labelled d - 1. Cost stays O(R x C).

Part 3: Limited battery

With a battery, arriving at a cell is not the whole story: getting there with more charge left opens moves that a weaker arrival cannot make. So the search node becomes (cell, charge). From a node with charge >= 1, each open neighbour costs one unit, except that landing on C resets the charge to capacity. A node with 0 charge is a dead end and is simply not expanded. The visited set holds the full pair, so the same cell can be queued once per distinct charge level, which is what lets a detour through a charger beat a direct path that runs dry. BFS still applies because every move costs one. There are at most R x C x (capacity + 1) states, which bounds both time and memory.

Part 4: No grid edges

On an endless plane BFS has no natural stopping point, so an enclosed end would loop forever. The fix is a finite search window. Take the smallest box containing start, end and every blocked point, and grow it by one cell on each side. Outside the original box there are no obstacles, and the added ring is entirely open, so any route that wanders further out can be pressed back onto that ring without getting longer. That means BFS restricted to the grown box finds the true shortest distance, and it finishes because the box is finite. The reference checks bounds and blocked for each neighbour and returns -1 when the frontier empties. Cost is proportional to the area of the box.

Hints

Part 1 hint

All moves cost one, so breadth-first search reaches each cell first along a shortest route. Add a cell to the visited set when it is put on the queue.

Part 2 hint

Run the search from E to label every cell with its distance to E. Then from S, keep stepping to the smallest neighbour (row, then column) whose label is one lower.

Part 3 hint

Position alone is no longer enough: reaching a cell with 3 units left is better than with 1. Search over (cell, charge left) and keep that pair in the visited set.

Part 4 hint

An open plane makes BFS run forever when there is no path. Limit the search to the bounding box of start, end and every blocked point, grown by one cell on each side.

Common mistakes

  • Marking a cell visited only when it leaves the queue, so it gets added many times over.
  • Keeping visited as plain (row, col) in part 3, which throws away a later visit that arrives with more charge.
  • Letting a move happen with 0 charge, or refilling before the move is paid for.
  • Running BFS on the infinite grid with no bound, which never ends when the end is enclosed.
  • Bounding the infinite grid to exactly the box of all points, with no extra margin, which misses paths that must go around a wall.

Where an interviewer could take it next

  • Each cell has a different move cost. What replaces BFS?
  • You may break through at most k walls. How does the state change?
  • Several start cells: find the distance from the nearest one to every cell.

Complexity

Parts 1 and 2 visit each cell at most once: O(R x C) time and space for an R by C grid. Part 3 searches (cell, charge) pairs, O(R x C x (capacity + 1)). Part 4 is bounded by the box around the B blocked points plus start and end, O(W x H) for that box; a box that is huge but mostly empty is where an A* search with a Manhattan-distance estimate would help.

Frequently asked

Why does BFS give the shortest path in an unweighted grid?
BFS processes cells in order of distance from the start: first all cells one move away, then all cells two moves away, and so on. When a cell is reached for the first time, no shorter route to it can exist, because every shorter distance was fully explored earlier. That guarantee needs every move to cost the same. With different costs per move you would switch to Dijkstra's algorithm.
How do you reconstruct the shortest path after BFS?
Two common ways work. Store a parent for each cell when it is first discovered, then follow parents back from the end and reverse the list. Or run BFS from the end to get each cell's distance to it, and walk forward from the start, always stepping to a neighbour whose distance is one lower. The second makes a fixed tie-break easy, since you choose among valid next cells at each step.
How do you do BFS with fuel or battery limits?
Add the remaining fuel to the state. Each node is (cell, fuel left), every move lowers fuel by one, and a refuelling cell sets it back to full. Mark the pair as visited rather than the cell, because reaching a cell with more fuel can lead somewhere a low-fuel arrival cannot. When every move costs the same, plain BFS over these pairs still gives the fewest moves.

Practise next

See what candidates report at each company

The company guides collect the questions candidates report, with every part, the follow-ups interviewers added, and how often each one comes up.

Browse the interview guides
Is this helpful?