Facebook Pixel

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:

  1. It explores the grid level by level from the starting position
  2. The first food cell encountered is guaranteed to be at the shortest distance
  3. All moves have equal cost (unweighted graph)
  4. BFS naturally finds the shortest path in unweighted graphs
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 Evaluator

Example 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
  • Process cell (1, 0):
    • Check down: (2, 0) has '#'Food found! Return 2

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:

  1. We use BFS to explore the grid level by level, ensuring we find the shortest path
  2. The pairwise(dirs) trick generates direction pairs: (-1,0), (0,1), (1,0), (0,-1)
  3. We mark visited cells by changing 'O' to 'X' in-place to avoid cycles
  4. The moment we encounter food '#', we return immediately since BFS guarantees this is the shortest distance
  5. 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))
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More