Shortest path through a grid
A grid is a list of equal-length strings of '.' (open) and '#' (wall). Starting
in the top-left cell, you may move up, down, left or right into open cells. Return the fewest moves needed to
reach the bottom-right cell, or -1 if it cannot be reached. If either corner is a wall, return
-1.
Examples
| Input | Output | Why |
|---|---|---|
grid = ["..#", ".#.", "..."] | 4 | Down, down, right, right: the wall in the middle forces the long way round. |
grid = [".#", "#."] | -1 | The corners are open but nothing connects them. |
grid = ["."] | 0 | The start is the goal. |
Constraints
- 1 ≤ rows, cols ≤ 500
- Every row has the same length
- Cells are '.' or '#'
Hints
Hint 1
Each open cell is a node; each move is an edge of length 1. You want a shortest path in an unweighted graph.
Hint 2
Breadth-first search visits cells in order of their distance from the start, so the first time it reaches the goal is by a shortest path.
Hint 3
Process the queue one ring at a time (or store a distance per cell), and mark cells as seen when you enqueue them.
Where people lose marks
- Using depth-first search, which finds a path but not the shortest one.
- Marking cells when they are dequeued instead of enqueued: the same cell can then sit in the queue many times.
- Using a list with pop(0) as the queue in Python, which makes each removal O(n). Use collections.deque.
- Forgetting the cases where a corner is a wall, or the grid is a single cell.
Dry run on ["..#",".#.","..."]
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 5row 000.1#2row 1.0#1.2row 2.0.1.2distance0
Start in the top-left corner at distance 0.
-
Step 2 / 5row 00011#2row 110#1.2row 2.0.1.2distance1
Ring 1: 2 new cells are one step beyond the last ring. Each is marked as it is queued.
-
Step 3 / 5row 00011#2row 110#1.2row 220.1.2distance2
Ring 2: 1 new cell is one step beyond the last ring. Each is marked as it is queued.
-
Step 4 / 5row 00011#2row 110#1.2row 22031.2distance3
Ring 3: 1 new cell is one step beyond the last ring. Each is marked as it is queued.
-
Step 5 / 5row 00011#2row 110#1.2row 2203142distance4
Ring 4 reaches the bottom-right corner. Nothing closer could have reached it, so the answer is 4.
How to think about it
With every move costing the same, breadth-first search is exactly a shortest-path algorithm. It holds a queue that always contains cells at distance d followed by cells at distance d + 1, so cells leave the queue in non-decreasing order of distance, and the first time the goal leaves it, its distance is minimal.
- Start with the top-left cell at distance 0, marked seen.
- Repeatedly take the front cell; for each open, unseen neighbour, record distance + 1, mark it, enqueue it.
- Return the goal's distance when you reach it, or −1 when the queue runs dry.
Depth-first search would also find a path, but not the shortest, and making it find the shortest means exploring every path: exponential time on an open grid.
Complexity. Time O(rows · cols) · Space O(rows · cols)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
from collections import deque
def shortest_path(grid):
rows, cols = len(grid), len(grid[0])
if grid[0][0] == '#' or grid[-1][-1] == '#':
return -1
dist = [[-1] * cols for _ in range(rows)]
dist[0][0] = 0
q = deque([(0, 0)])
while q:
i, j = q.popleft()
if (i, j) == (rows - 1, cols - 1):
return dist[i][j] # BFS reaches every cell first by a shortest path
for a, b in ((i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)):
if 0 <= a < rows and 0 <= b < cols and grid[a][b] == '.' and dist[a][b] < 0:
dist[a][b] = dist[i][j] + 1 # mark on enqueue
q.append((a, b))
return -1#include <queue>
#include <string>
#include <utility>
#include <vector>
int solve(const std::vector<std::string>& grid) {
const int rows = grid.size(), cols = grid[0].size();
if (grid[0][0] == '#' || grid[rows - 1][cols - 1] == '#') return -1;
std::vector<std::vector<int>> dist(rows, std::vector<int>(cols, -1));
const int dr[] = {1, -1, 0, 0}, dc[] = {0, 0, 1, -1};
std::queue<std::pair<int, int>> q;
dist[0][0] = 0;
q.push({0, 0});
while (!q.empty()) {
auto [i, j] = q.front();
q.pop();
if (i == rows - 1 && j == cols - 1) return dist[i][j];
for (int k = 0; k < 4; ++k) {
const int a = i + dr[k], b = j + dc[k];
if (a >= 0 && a < rows && b >= 0 && b < cols && grid[a][b] == '.' && dist[a][b] < 0) {
dist[a][b] = dist[i][j] + 1; // mark on enqueue
q.push({a, b});
}
}
}
return -1;
}class Solution {
public int shortestPath(String[] grid) {
int rows = grid.length, cols = grid[0].length();
if (grid[0].charAt(0) == '#' || grid[rows - 1].charAt(cols - 1) == '#') return -1;
int[][] dist = new int[rows][cols];
for (int[] row : dist) java.util.Arrays.fill(row, -1);
int[] dr = {1, -1, 0, 0}, dc = {0, 0, 1, -1};
java.util.ArrayDeque<int[]> q = new java.util.ArrayDeque<>();
dist[0][0] = 0;
q.add(new int[]{0, 0});
while (!q.isEmpty()) {
int[] cell = q.poll();
int i = cell[0], j = cell[1];
if (i == rows - 1 && j == cols - 1) return dist[i][j];
for (int k = 0; k < 4; k++) {
int a = i + dr[k], b = j + dc[k];
if (a >= 0 && a < rows && b >= 0 && b < cols && grid[a].charAt(b) == '.' && dist[a][b] < 0) {
dist[a][b] = dist[i][j] + 1; // mark on enqueue
q.add(new int[]{a, b});
}
}
}
return -1;
}
}function shortestPath(grid) {
const rows = grid.length, cols = grid[0].length;
if (grid[0][0] === '#' || grid[rows - 1][cols - 1] === '#') return -1;
const dist = Array.from({ length: rows }, () => new Array(cols).fill(-1));
dist[0][0] = 0;
const q = [[0, 0]];
for (let head = 0; head < q.length; head++) { // an index instead of shift(): O(1) per pop
const [i, j] = q[head];
if (i === rows - 1 && j === cols - 1) return dist[i][j];
for (const [a, b] of [[i + 1, j], [i - 1, j], [i, j + 1], [i, j - 1]]) {
if (a >= 0 && a < rows && b >= 0 && b < cols && grid[a][b] === '.' && dist[a][b] < 0) {
dist[a][b] = dist[i][j] + 1; // mark on enqueue
q.push([a, b]);
}
}
}
return -1;
}use std::collections::VecDeque;
fn solve(grid: &[&str]) -> i64 {
let g: Vec<&[u8]> = grid.iter().map(|row| row.as_bytes()).collect();
let (rows, cols) = (g.len(), g[0].len());
if g[0][0] == b'#' || g[rows - 1][cols - 1] == b'#' {
return -1;
}
let mut dist = vec![vec![-1i64; cols]; rows];
dist[0][0] = 0;
let mut q = VecDeque::from([(0usize, 0usize)]);
while let Some((i, j)) = q.pop_front() {
if i == rows - 1 && j == cols - 1 {
return dist[i][j];
}
let mut next = Vec::with_capacity(4);
if i > 0 { next.push((i - 1, j)); }
if i + 1 < rows { next.push((i + 1, j)); }
if j > 0 { next.push((i, j - 1)); }
if j + 1 < cols { next.push((i, j + 1)); }
for (a, b) in next {
if g[a][b] == b'.' && dist[a][b] < 0 {
dist[a][b] = dist[i][j] + 1; // mark on enqueue
q.push_back((a, b));
}
}
}
-1
}import qualified Data.Set as S
-- Breadth-first search one ring at a time; the ring number is the distance.
solve :: [String] -> Int
solve grid
| not (open (0, 0)) || not (open goal) = -1
| otherwise = go 0 [(0, 0)] (S.singleton (0, 0))
where
rows = length grid
cols = length (head grid)
goal = (rows - 1, cols - 1)
cells = S.fromList [(r, c) | (r, row) <- zip [0 ..] grid, (c, ch) <- zip [0 ..] row, ch == '.']
open q = q `S.member` cells
go _ [] _ = -1
go d ring seen
| goal `elem` ring = d
| otherwise =
let next = S.toList (S.fromList [q | (r, c) <- ring, q <- [(r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)], open q, not (q `S.member` seen)])
in go (d + 1) next (foldr S.insert seen next)
Variants to try
- Allow diagonal moves as well. What changes?
- You may knock down at most k walls. Add the number of walls broken to the state.
- Cells have entry costs 1 to 9: switch to Dijkstra, or to 0-1 BFS if costs are only 0 and 1.
- Return the path itself, not just its length, by storing each cell's parent.
Next problems
Distance to the nearest zero
For every cell of a 0/1 grid, return the number of steps to the nearest 0. One breadth-first search that starts from all the zeros at once.
Open the problem → Medium18 minMinutes until every orange rots
Start the search from every rotten orange at once. The answer is the number of rings it takes to reach the last fresh one.
Open the problem → Hard30 minShortest word transformation
Change one letter at a time, using only dictionary words, from a start word to an end word. Each word is a node; breadth-first search finds the shortest ladder.
Open the problem →Write a solution and run it against the real test table.
Keep going
Distance to the nearest zero
For every cell of a 0/1 grid, return the number of steps to the nearest 0. One breadth-first search that starts from all the zeros at once.
Open the problem → Medium18 minMinutes until every orange rots
Start the search from every rotten orange at once. The answer is the number of rings it takes to reach the last fresh one.
Open the problem → Hard30 minShortest word transformation
Change one letter at a time, using only dictionary words, from a start word to an end word. Each word is a node; breadth-first search finds the shortest ladder.
Open the problem → GraphsThe pattern behind it
Grids, networks and dependencies are all graphs. Breadth-first search finds shortest paths, depth-first search and union-find find what is connected, Dijkstra handles weighted edges, and a queue of nodes with no remaining prerequisites gives an order.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.