1730. Shortest Path to Get Food
Problem Description
You are placed in a grid and need to find the shortest path to reach any food cell. The grid is an m x n
character matrix containing four types of cells:
'*'
represents your starting location (there is exactly one such cell)'#'
represents a food cell (there can be multiple food cells)'O'
represents free space that you can travel through'X'
represents an obstacle that blocks your path
You can move in four directions from any cell: north (up), east (right), south (down), or west (left). You can only move to an adjacent cell if it's within the grid boundaries and is not an obstacle.
The goal is to find the length of the shortest path from your starting position to any food cell. If no path exists to reach any food, return -1
.
For example, if you start at '*'
and the nearest food '#'
is 3 moves away (moving through free spaces 'O'
), you would return 3
. The path length counts the number of moves needed, not the number of cells visited.
This is a classic shortest path problem where you need to explore the grid level by level from your starting position until you encounter the first food cell, which will guarantee the shortest distance due to the nature of breadth-first traversal.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The grid can be viewed as a graph where each cell is a node, and adjacent cells (up, down, left, right) are connected by edges if they're not obstacles.
Is it a tree?
- No: The grid structure allows multiple paths between cells and can have cycles, so it's not a tree.
Is the problem related to directed acyclic graphs?
- No: The grid allows bidirectional movement between adjacent cells, and cycles are possible.
Is the problem related to shortest paths?
- Yes: We need to find the shortest path from the starting position
'*'
to any food cell'#'
.
Is the graph Weighted?
- No: Each move from one cell to an adjacent cell has the same cost (1 step). All edges have equal weight.
BFS
- Yes: Since we're dealing with an unweighted graph and need the shortest path, BFS is the optimal choice.
Conclusion: The flowchart correctly leads us to use BFS (Breadth-First Search) for this problem. BFS is perfect here because:
- It explores the grid level by level from the starting position
- The first food cell encountered is guaranteed to be at the shortest distance
- All moves have equal cost (unweighted graph)
- BFS naturally finds the shortest path in unweighted graphs
Intuition
When we need to find the shortest path in a grid, we should think about how we explore the space. If we randomly walk around or use depth-first search, we might find a food cell, but it won't necessarily be the closest one. We could end up taking a long winding path when a shorter one exists.
The key insight is that we want to explore the grid in "layers" or "waves" emanating from our starting position. Think of it like dropping a stone in water - the ripples spread outward uniformly in all directions. This is exactly what BFS does.
Starting from '*'
, we first check all cells that are 1 step away. If none of them contain food, we then check all cells that are 2 steps away, then 3 steps away, and so on. The moment we encounter a food cell '#'
, we know we've found it at the minimum possible distance because we've already checked all cells that were closer.
Why does this guarantee the shortest path? Because BFS explores cells in order of their distance from the start. When we reach distance d
, we've already explored all cells at distances 0, 1, 2, ..., d-1
. So if there was a food cell at a shorter distance, we would have already found it.
To implement this, we use a queue to maintain the "frontier" of our exploration. We also need to mark visited cells (by changing 'O'
to 'X'
in this solution) to avoid revisiting them and getting stuck in cycles. Each "layer" of BFS corresponds to incrementing our distance counter by 1.
The beauty of this approach is its simplicity and optimality - we're guaranteed to find the shortest path with just a straightforward level-by-level exploration, making it perfect for unweighted grid problems where all moves have the same cost.
Learn more about Breadth-First Search patterns.
Solution Approach
Following the BFS approach identified through our flowchart analysis, let's walk through the implementation step by step:
1. Find the Starting Position
First, we need to locate the '*'
cell in the grid. The solution uses a generator expression with next()
to efficiently find it:
i, j = next((i, j) for i in range(m) for j in range(n) if grid[i][j] == '*')
This gives us our starting coordinates (i, j)
.
2. Initialize BFS Data Structures
We set up a queue (using deque
for efficient pop from the left) and add our starting position:
q = deque([(i, j)])
The direction array is cleverly defined to help generate the four adjacent cells:
dirs = (-1, 0, 1, 0, -1)
Using pairwise(dirs)
generates pairs: (-1, 0)
, (0, 1)
, (1, 0)
, (0, -1)
which represent movements up, right, down, and left respectively.
3. Level-by-Level BFS Traversal
The key to tracking distance is processing the queue level by level:
while q:
ans += 1 # Increment distance for each new level
for _ in range(len(q)): # Process all cells at current distance
i, j = q.popleft()
This ensures all cells at distance d
are processed before moving to distance d+1
.
4. Explore Adjacent Cells
For each cell, we check all four adjacent positions:
for a, b in pairwise(dirs): x, y = i + a, j + b if 0 <= x < m and 0 <= y < n: # Check bounds
5. Handle Different Cell Types
-
If we find food (
'#'
), immediately return the current distance:if grid[x][y] == '#': return ans
-
If it's a free space (
'O'
), mark it as visited and add to queue:if grid[x][y] == 'O': grid[x][y] = 'X' # Mark as visited q.append((x, y))
The clever optimization here is modifying the grid in-place to mark visited cells instead of using a separate visited set. By changing 'O'
to 'X'
, we prevent revisiting cells without extra memory.
6. No Path Found
If the queue becomes empty without finding food, return -1
:
return -1
Time and Space Complexity:
- Time:
O(m × n)
where we potentially visit every cell once - Space:
O(min(m × n, length of perimeter))
for the queue, which in the worst case could hold all cells at a particular distance level
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the BFS solution approach:
Grid: ['*', 'O', 'O'] ['O', 'X', 'O'] ['#', 'O', '#']
Initial Setup:
- Find starting position:
(0, 0)
where'*'
is located - Initialize queue:
q = [(0, 0)]
- Distance counter:
ans = 0
Level 1 (distance = 1):
- Increment
ans = 1
- Process cell
(0, 0)
:- Check up: Out of bounds
- Check right:
(0, 1)
has'O'
→ Mark as'X'
, add to queue - Check down:
(1, 0)
has'O'
→ Mark as'X'
, add to queue - Check left: Out of bounds
- Queue now:
[(0, 1), (1, 0)]
- Grid now:
['*', 'X', 'O'] ['X', 'X', 'O'] ['#', 'O', '#']
Level 2 (distance = 2):
- Increment
ans = 2
- Process cell
(0, 1)
:- Check all directions:
(0, 2)
has'O'
→ Mark as'X'
, add to queue
- Check all directions:
- Process cell
(1, 0)
:- Check down:
(2, 0)
has'#'
→ Food found! Return 2
- Check down:
The algorithm returns 2
as the shortest path length to reach any food cell.
Template Solution
from collections import deque
from itertools import pairwise
class Solution:
def getFood(self, grid: List[List[str]]) -> int:
# Step 1: Get grid dimensions and find starting position
m, n = len(grid), len(grid[0])
i, j = next((i, j) for i in range(m) for j in range(n) if grid[i][j] == '*')
# Step 2: Initialize BFS queue with starting position
q = deque([(i, j)])
# Direction vectors for moving up, right, down, left
dirs = (-1, 0, 1, 0, -1)
# Distance counter
ans = 0
# Step 3: BFS traversal
while q:
ans += 1 # Increment distance for each level
# Process all cells at current distance level
for _ in range(len(q)):
i, j = q.popleft()
# Check all 4 adjacent cells
for a, b in pairwise(dirs):
x, y = i + a, j + b
# Check if within bounds
if 0 <= x < m and 0 <= y < n:
# Found food - return current distance
if grid[x][y] == '#':
return ans
# Found free space - mark visited and add to queue
if grid[x][y] == 'O':
grid[x][y] = 'X' # Mark as visited
q.append((x, y))
# No path to food found
return -1
Key Points:
- We use BFS to explore the grid level by level, ensuring we find the shortest path
- The
pairwise(dirs)
trick generates direction pairs:(-1,0)
,(0,1)
,(1,0)
,(0,-1)
- We mark visited cells by changing
'O'
to'X'
in-place to avoid cycles - The moment we encounter food
'#'
, we return immediately since BFS guarantees this is the shortest distance - If the queue empties without finding food, no path exists and we return
-1
Solution Implementation
1class Solution:
2 def getFood(self, grid: List[List[str]]) -> int:
3 # Get grid dimensions
4 rows, cols = len(grid), len(grid[0])
5
6 # Find the starting position marked with '*'
7 start_row, start_col = next(
8 (row, col)
9 for row in range(rows)
10 for col in range(cols)
11 if grid[row][col] == '*'
12 )
13
14 # Initialize BFS queue with starting position
15 queue = deque([(start_row, start_col)])
16
17 # Direction vectors for moving up, right, down, left
18 # Using pairwise iteration: (-1,0), (0,1), (1,0), (0,-1)
19 directions = (-1, 0, 1, 0, -1)
20
21 # Track the number of steps taken
22 steps = 0
23
24 # BFS to find shortest path to food
25 while queue:
26 steps += 1
27
28 # Process all cells at current distance level
29 for _ in range(len(queue)):
30 current_row, current_col = queue.popleft()
31
32 # Check all 4 adjacent cells
33 for delta_row, delta_col in pairwise(directions):
34 next_row = current_row + delta_row
35 next_col = current_col + delta_col
36
37 # Check if the next position is within grid bounds
38 if 0 <= next_row < rows and 0 <= next_col < cols:
39 # Found food - return the number of steps
40 if grid[next_row][next_col] == '#':
41 return steps
42
43 # If empty space, mark as visited and add to queue
44 if grid[next_row][next_col] == 'O':
45 grid[next_row][next_col] = 'X' # Mark as visited
46 queue.append((next_row, next_col))
47
48 # No path to food found
49 return -1
50
1class Solution {
2 // Direction vectors for moving up, right, down, left (4-directional movement)
3 private int[] directions = {-1, 0, 1, 0, -1};
4
5 public int getFood(char[][] grid) {
6 int rows = grid.length;
7 int cols = grid[0].length;
8
9 // Queue for BFS traversal, storing coordinates as int arrays
10 Deque<int[]> queue = new ArrayDeque<>();
11
12 // Find the starting position marked with '*'
13 boolean startFound = false;
14 for (int row = 0; row < rows && !startFound; row++) {
15 for (int col = 0; col < cols; col++) {
16 if (grid[row][col] == '*') {
17 queue.offer(new int[] {row, col});
18 startFound = true;
19 break;
20 }
21 }
22 }
23
24 // Track the number of steps taken
25 int steps = 0;
26
27 // BFS to find shortest path to food
28 while (!queue.isEmpty()) {
29 steps++;
30
31 // Process all cells at current distance level
32 int currentLevelSize = queue.size();
33 for (int i = 0; i < currentLevelSize; i++) {
34 int[] currentPosition = queue.poll();
35
36 // Explore all 4 adjacent directions
37 for (int dir = 0; dir < 4; dir++) {
38 int nextRow = currentPosition[0] + directions[dir];
39 int nextCol = currentPosition[1] + directions[dir + 1];
40
41 // Check if the next position is within grid boundaries
42 if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
43 // Found food, return the number of steps
44 if (grid[nextRow][nextCol] == '#') {
45 return steps;
46 }
47
48 // If it's an open space, mark as visited and add to queue
49 if (grid[nextRow][nextCol] == 'O') {
50 grid[nextRow][nextCol] = 'X'; // Mark as visited
51 queue.offer(new int[] {nextRow, nextCol});
52 }
53 }
54 }
55 }
56 }
57
58 // No path to food found
59 return -1;
60 }
61}
62
1class Solution {
2public:
3 // Direction vectors for moving up, right, down, left
4 const static inline vector<int> directions = {-1, 0, 1, 0, -1};
5
6 int getFood(vector<vector<char>>& grid) {
7 int rows = grid.size();
8 int cols = grid[0].size();
9 queue<pair<int, int>> bfsQueue;
10
11 // Find the starting position marked with '*'
12 bool found = false;
13 for (int row = 0; row < rows && !found; ++row) {
14 for (int col = 0; col < cols; ++col) {
15 if (grid[row][col] == '*') {
16 bfsQueue.emplace(row, col);
17 found = true;
18 break;
19 }
20 }
21 }
22
23 // BFS to find shortest path to food
24 int steps = 0;
25 while (!bfsQueue.empty()) {
26 ++steps;
27 int currentLevelSize = bfsQueue.size();
28
29 // Process all cells at current distance level
30 for (int i = 0; i < currentLevelSize; ++i) {
31 auto [currentRow, currentCol] = bfsQueue.front();
32 bfsQueue.pop();
33
34 // Check all 4 adjacent cells
35 for (int dir = 0; dir < 4; ++dir) {
36 int nextRow = currentRow + directions[dir];
37 int nextCol = currentCol + directions[dir + 1];
38
39 // Check if the next position is within grid bounds
40 if (nextRow >= 0 && nextRow < rows &&
41 nextCol >= 0 && nextCol < cols) {
42
43 // Found food, return the number of steps
44 if (grid[nextRow][nextCol] == '#') {
45 return steps;
46 }
47
48 // If it's an empty space, mark as visited and add to queue
49 if (grid[nextRow][nextCol] == 'O') {
50 grid[nextRow][nextCol] = 'X'; // Mark as visited
51 bfsQueue.emplace(nextRow, nextCol);
52 }
53 }
54 }
55 }
56 }
57
58 // No path to food found
59 return -1;
60 }
61};
62
1/**
2 * Finds the shortest path from the starting position (*) to food (#) in a grid
3 * @param grid - 2D character array where '*' is start, '#' is food, 'O' is free space, 'X' is obstacle
4 * @returns The minimum number of steps to reach food, or -1 if impossible
5 */
6function getFood(grid: string[][]): number {
7 const rows: number = grid.length;
8 const cols: number = grid[0].length;
9
10 // Direction vectors for moving up, right, down, left
11 const directions: number[] = [-1, 0, 1, 0, -1];
12
13 // Queue for BFS traversal
14 const queue: [number, number][] = [];
15
16 // Find the starting position marked with '*'
17 let startFound: boolean = false;
18 for (let row = 0; row < rows && !startFound; row++) {
19 for (let col = 0; col < cols; col++) {
20 if (grid[row][col] === '*') {
21 queue.push([row, col]);
22 startFound = true;
23 break;
24 }
25 }
26 }
27
28 let steps: number = 0;
29
30 // Perform BFS to find shortest path to food
31 while (queue.length > 0) {
32 steps++;
33
34 // Process all cells at current distance level
35 const currentLevelSize: number = queue.length;
36 for (let i = 0; i < currentLevelSize; i++) {
37 const [currentRow, currentCol] = queue.shift()!;
38
39 // Explore all 4 adjacent directions
40 for (let dir = 0; dir < 4; dir++) {
41 const nextRow: number = currentRow + directions[dir];
42 const nextCol: number = currentCol + directions[dir + 1];
43
44 // Check if next position is within grid bounds
45 if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
46 // Found food
47 if (grid[nextRow][nextCol] === '#') {
48 return steps;
49 }
50
51 // Mark free space as visited and add to queue
52 if (grid[nextRow][nextCol] === 'O') {
53 grid[nextRow][nextCol] = 'X';
54 queue.push([nextRow, nextCol]);
55 }
56 }
57 }
58 }
59 }
60
61 // No path to food found
62 return -1;
63}
64
Time and Space Complexity
The time complexity is O(m × n)
, where m
is the number of rows and n
is the number of columns in the grid. In the worst case, the BFS algorithm needs to visit every cell in the grid exactly once to find the food. Each cell is added to the queue at most once (when it's an empty cell 'O' that gets marked as visited 'X'), and each cell is processed at most once when popped from the queue.
The space complexity is O(min(m, n))
, not O(1)
as stated in the reference answer. The reference answer appears to be incorrect here. The space complexity is determined by the maximum size of the BFS queue. In the worst case, the queue can contain all cells at a particular "layer" of the BFS traversal. This happens when the BFS expands in a diamond or rectangular pattern, and the maximum number of cells in the queue at any time is proportional to the perimeter of the explored region, which is bounded by O(min(m, n))
. Additionally, the dirs
tuple uses constant space O(1)
, but the queue dominates the space complexity.
Note: The algorithm modifies the input grid in-place by marking visited cells with 'X', which allows it to avoid using a separate visited set. However, the queue itself still requires O(min(m, n))
space in the worst case.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Marking the Starting Position as Visited
One of the most common mistakes is forgetting to mark the starting position ('*'
) as visited after processing it. This can lead to revisiting the starting cell and creating infinite loops or incorrect distance calculations.
Problematic Code:
# Starting position is added to queue but never marked as visited queue = deque([(start_row, start_col)]) # The '*' cell remains unchanged in the grid
Solution: Mark the starting position as visited immediately after adding it to the queue:
queue = deque([(start_row, start_col)]) grid[start_row][start_col] = 'X' # Mark starting position as visited
2. Incrementing Distance Before Processing Any Cells
Another subtle bug occurs when incrementing the distance counter at the wrong point in the BFS loop. The current solution increments steps
at the beginning of each level, which means even if food is found at distance 0 (impossible in this problem but conceptually important), it would return 1.
Problematic Pattern:
while queue:
steps += 1 # Incremented before checking any cells
for _ in range(len(queue)):
# If food is adjacent to start, returns 1 instead of 0
Better Solution: Increment the counter after processing each level or handle the first iteration separately:
steps = 0
while queue:
current_level_size = len(queue)
for _ in range(current_level_size):
current_row, current_col = queue.popleft()
# Check all adjacent cells
for delta_row, delta_col in pairwise(directions):
next_row = current_row + delta_row
next_col = current_col + delta_col
if 0 <= next_row < rows and 0 <= next_col < cols:
if grid[next_row][next_col] == '#':
return steps + 1 # Return distance to food
if grid[next_row][next_col] == 'O':
grid[next_row][next_col] = 'X'
queue.append((next_row, next_col))
steps += 1 # Increment after processing the level
3. Marking Cells as Visited After Dequeuing Instead of Before Enqueuing
A critical performance issue occurs when cells are marked as visited only when they're dequeued rather than when they're first discovered and added to the queue.
Problematic Code:
while queue: current_row, current_col = queue.popleft() grid[current_row][current_col] = 'X' # Marking here is too late! for delta_row, delta_col in pairwise(directions): next_row = current_row + delta_row next_col = current_col + delta_col if 0 <= next_row < rows and 0 <= next_col < cols: if grid[next_row][next_col] == 'O': queue.append((next_row, next_col)) # Cell not marked yet
This causes the same cell to be added to the queue multiple times from different paths, leading to:
- Exponential time complexity in worst case
- Incorrect distance calculations
- Potential memory issues with large grids
Correct Approach: Always mark cells as visited immediately when adding them to the queue:
if grid[next_row][next_col] == 'O': grid[next_row][next_col] = 'X' # Mark immediately queue.append((next_row, next_col))
What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!