Facebook Pixel

1293. Shortest Path in a Grid with Obstacles Elimination

Problem Description

You have an m x n grid where each cell contains either a 0 (representing an empty cell) or a 1 (representing an obstacle). Your task is to find the shortest path from the top-left corner (0, 0) to the bottom-right corner (m-1, n-1).

Movement rules:

  • You can move in four directions: up, down, left, or right
  • Each move counts as one step
  • You can only move to empty cells (0)
  • You cannot normally move through obstacles (1)

Special ability:

  • You have the power to eliminate up to k obstacles
  • This means you can walk through at most k cells that contain 1, effectively treating them as 0

The goal is to return the minimum number of steps needed to reach the destination. If it's impossible to reach the destination even after eliminating up to k obstacles, return -1.

For example, if you have a 3x3 grid with some obstacles and k=2, you need to find the shortest path that goes through at most 2 obstacles to get from the top-left to the bottom-right corner.

The solution uses BFS (Breadth-First Search) to explore all possible paths. The key insight is to track not just the position (i, j) but also the remaining obstacle elimination count at each position. The state (i, j, k) represents being at position (i, j) with k eliminations remaining. The algorithm explores all reachable states level by level, ensuring the first time we reach the destination is via the shortest path.

The optimization if k >= m + n - 3: return m + n - 2 handles cases where we have enough eliminations to simply go straight right and then down (or down then right), which would be the shortest possible path of m + n - 2 steps.

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 edges connect adjacent cells (up, down, left, right). We need to traverse from one node (top-left) to another (bottom-right).

Is it a tree?

  • No: The grid structure is not a tree. It contains cycles (you can move in circles) and multiple paths between nodes.

Is the problem related to directed acyclic graphs?

  • No: The grid allows bidirectional movement (you can move back and forth between cells), so it's not a DAG.

Is the problem related to shortest paths?

  • Yes: We need to find the minimum number of steps (shortest path) from the starting position to the ending position.

Is the graph Weighted?

  • No: Each move counts as exactly one step regardless of which cell we move to. All edges have the same weight (1). The obstacle elimination is a constraint on which paths are valid, not a weight on the edges.

BFS

  • Yes: Since we have an unweighted graph and need the shortest path, BFS is the appropriate algorithm.

Conclusion: The flowchart suggests using BFS (Breadth-First Search) for finding the shortest path in this unweighted grid graph. The key insight is that we treat the problem as a state-space search where each state is (row, column, remaining_eliminations), and BFS guarantees we find the shortest path first due to its level-by-level exploration.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that this isn't just a simple shortest path problem - we have a special power to eliminate obstacles. This changes how we think about the problem.

In a standard grid shortest path problem, we'd use BFS and track visited cells with a 2D array. But here, visiting the same cell (i, j) with different amounts of remaining eliminations represents fundamentally different states. For instance, reaching cell (2, 3) with 5 eliminations left is different from reaching it with only 1 elimination left - the first state gives us more flexibility for the rest of the path.

Think of it like having a limited number of "wall-breaking hammers". Each time we encounter an obstacle, we can choose to use a hammer to break through it. The challenge is that we might need to save our hammers for obstacles that block the only path to the destination.

This leads us to expand our state representation from just (row, column) to (row, column, remaining_eliminations). Each unique combination represents a different state in our search space. We use BFS because:

  1. Level-by-level exploration: BFS explores all cells at distance d before moving to distance d+1, guaranteeing the first time we reach the destination is via the shortest path.

  2. State tracking: We track visited states as (i, j, k) tuples to avoid revisiting the same position with the same elimination count, preventing infinite loops while allowing revisits with different elimination counts.

  3. Branching logic: At each cell, we can move to an empty neighbor (keeping our elimination count) or move through an obstacle (decreasing our elimination count by 1).

The optimization if k >= m + n - 3 is clever: if we have enough eliminations to break through all obstacles on the Manhattan distance path (the straight right-then-down or down-then-right path), we can directly return m + n - 2 steps, which is the theoretical minimum for any grid path from top-left to bottom-right.

Learn more about Breadth-First Search patterns.

Solution Approach

The implementation uses BFS with a modified state representation to handle the obstacle elimination constraint. Let's walk through the key components:

1. Initial Optimization Check

if k >= m + n - 3:
    return m + n - 2

This handles the case where we have enough eliminations to simply take the Manhattan distance path. Since the minimum steps from (0, 0) to (m-1, n-1) is m + n - 2, and the maximum obstacles we'd encounter on this path is m + n - 3 (excluding start and end), we can return immediately.

2. BFS Setup

q = deque([(0, 0, k)])
vis = {(0, 0, k)}
ans = 0
  • Initialize a queue with the starting state: position (0, 0) with k eliminations available
  • Use a set vis to track visited states as 3-tuples (row, col, remaining_eliminations)
  • ans tracks the current distance/steps

3. Level-by-Level BFS Traversal

while q:
    ans += 1
    for _ in range(len(q)):
        i, j, k = q.popleft()

Process all nodes at the current level before moving to the next level. This ensures we find the shortest path.

4. Exploring Neighbors

for a, b in [[0, -1], [0, 1], [1, 0], [-1, 0]]:
    x, y = i + a, j + b

Check all four directions (up, down, right, left) from the current position.

5. Boundary and Goal Check

if 0 <= x < m and 0 <= y < n:
    if x == m - 1 and y == n - 1:
        return ans

First verify the new position is within bounds. If we've reached the destination, immediately return the current step count.

6. Movement Logic Two cases for valid moves:

Empty Cell Movement:

if grid[x][y] == 0 and (x, y, k) not in vis:
    q.append((x, y, k))
    vis.add((x, y, k))

Move to an empty cell without using eliminations. The state is (x, y, k) - same elimination count.

Obstacle Elimination:

if grid[x][y] == 1 and k > 0 and (x, y, k - 1) not in vis:
    q.append((x, y, k - 1))
    vis.add((x, y, k - 1))

Move through an obstacle if we have eliminations left. The new state is (x, y, k-1) - one less elimination available.

7. No Path Found

return -1

If the queue becomes empty without reaching the destination, no valid path exists within the elimination constraint.

The algorithm's time complexity is O(m * n * k) since we might visit each cell with different elimination counts, and space complexity is also O(m * n * k) for the visited set.

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 solution approach.

Consider a 3x3 grid with k=1 (we can eliminate at most 1 obstacle):

grid = [[0, 1, 0],
        [1, 1, 0],
        [0, 0, 0]]

Initial Setup:

  • Start at (0,0) with k=1 elimination available
  • Queue: [(0, 0, 1)]
  • Visited: {(0, 0, 1)}
  • Steps: 0

Step 1 (Distance = 1): From (0,0), we explore neighbors:

  • Right (0,1): Has obstacle (1). We can eliminate it using our power.
    • Add state (0, 1, 0) to queue (0 eliminations left after using one)
  • Down (1,0): Has obstacle (1). We can eliminate it.
    • Add state (1, 0, 0) to queue

Queue: [(0, 1, 0), (1, 0, 0)] Visited: {(0, 0, 1), (0, 1, 0), (1, 0, 0)}

Step 2 (Distance = 2): Process (0, 1, 0):

  • Right (0,2): Empty cell (0)
    • Add state (0, 2, 0) to queue
  • Down (1,1): Has obstacle (1), but k=0 (no eliminations left), so skip

Process (1, 0, 0):

  • Right (1,1): Has obstacle (1), but k=0, so skip
  • Down (2,0): Empty cell (0)
    • Add state (2, 0, 0) to queue

Queue: [(0, 2, 0), (2, 0, 0)] Visited: {(0, 0, 1), (0, 1, 0), (1, 0, 0), (0, 2, 0), (2, 0, 0)}

Step 3 (Distance = 3): Process (0, 2, 0):

  • Down (1,2): Empty cell (0)
    • Add state (1, 2, 0) to queue

Process (2, 0, 0):

  • Right (2,1): Empty cell (0)
    • Add state (2, 1, 0) to queue

Queue: [(1, 2, 0), (2, 1, 0)]

Step 4 (Distance = 4): Process (1, 2, 0):

  • Down (2,2): This is our destination (2, 2)!
    • Return current distance: 4

The shortest path is 4 steps: (0,0) β†’ (0,1) [eliminate] β†’ (0,2) β†’ (1,2) β†’ (2,2)

Key Observations:

  1. We tracked states as (row, col, remaining_eliminations), not just (row, col)
  2. Using our one elimination wisely at (0,1) opened up a path
  3. BFS guaranteed we found the shortest path first
  4. Alternative paths like going through (1,0) would require going around more obstacles, resulting in longer paths

Solution Implementation

1class Solution:
2    def shortestPath(self, grid: List[List[int]], k: int) -> int:
3        # Get grid dimensions
4        rows, cols = len(grid), len(grid[0])
5      
6        # Early optimization: if we have enough eliminations to go straight through
7        # The maximum obstacles we'd need to eliminate is (rows - 1) + (cols - 1) - 1
8        if k >= rows + cols - 3:
9            return rows + cols - 2
10      
11        # BFS queue: stores (row, col, remaining_eliminations)
12        queue = deque([(0, 0, k)])
13      
14        # Visited set: tracks (row, col, remaining_eliminations) states
15        visited = {(0, 0, k)}
16      
17        # Track current distance/steps
18        steps = 0
19      
20        # BFS to find shortest path
21        while queue:
22            steps += 1
23          
24            # Process all nodes at current distance level
25            for _ in range(len(queue)):
26                current_row, current_col, remaining_k = queue.popleft()
27              
28                # Check all 4 directions: up, down, right, left
29                directions = [[0, -1], [0, 1], [1, 0], [-1, 0]]
30                for delta_row, delta_col in directions:
31                    next_row = current_row + delta_row
32                    next_col = current_col + delta_col
33                  
34                    # Check if next position is within grid bounds
35                    if 0 <= next_row < rows and 0 <= next_col < cols:
36                        # Check if we reached the destination
37                        if next_row == rows - 1 and next_col == cols - 1:
38                            return steps
39                      
40                        # If next cell is empty (0), move without using elimination
41                        if grid[next_row][next_col] == 0:
42                            state = (next_row, next_col, remaining_k)
43                            if state not in visited:
44                                queue.append(state)
45                                visited.add(state)
46                      
47                        # If next cell is obstacle (1), use elimination if available
48                        elif grid[next_row][next_col] == 1 and remaining_k > 0:
49                            state = (next_row, next_col, remaining_k - 1)
50                            if state not in visited:
51                                queue.append(state)
52                                visited.add(state)
53      
54        # No path found
55        return -1
56
1class Solution {
2    public int shortestPath(int[][] grid, int k) {
3        int rows = grid.length;
4        int cols = grid[0].length;
5      
6        // Optimization: if we have enough eliminations to go straight through obstacles
7        // The minimum obstacles in any path is at most (rows - 1) + (cols - 1) - 1
8        if (k >= rows + cols - 3) {
9            return rows + cols - 2;
10        }
11      
12        // BFS queue storing [row, col, remaining eliminations]
13        Deque<int[]> queue = new ArrayDeque<>();
14        queue.offer(new int[] {0, 0, k});
15      
16        // 3D visited array: visited[row][col][remainingEliminations]
17        boolean[][][] visited = new boolean[rows][cols][k + 1];
18        visited[0][0][k] = true;
19      
20        // Track the number of steps taken
21        int steps = 0;
22      
23        // Direction vectors for moving up, right, down, left
24        int[] directions = {-1, 0, 1, 0, -1};
25      
26        // BFS to find shortest path
27        while (!queue.isEmpty()) {
28            steps++;
29          
30            // Process all cells at current distance level
31            int levelSize = queue.size();
32            for (int i = 0; i < levelSize; i++) {
33                int[] current = queue.poll();
34                int currentRow = current[0];
35                int currentCol = current[1];
36                int remainingEliminations = current[2];
37              
38                // Explore all 4 directions
39                for (int j = 0; j < 4; j++) {
40                    int nextRow = currentRow + directions[j];
41                    int nextCol = currentCol + directions[j + 1];
42                  
43                    // Check if next position is within grid bounds
44                    if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
45                        // Check if we reached the destination
46                        if (nextRow == rows - 1 && nextCol == cols - 1) {
47                            return steps;
48                        }
49                      
50                        // If next cell is empty and not visited with current elimination count
51                        if (grid[nextRow][nextCol] == 0 && !visited[nextRow][nextCol][remainingEliminations]) {
52                            queue.offer(new int[] {nextRow, nextCol, remainingEliminations});
53                            visited[nextRow][nextCol][remainingEliminations] = true;
54                        }
55                        // If next cell is obstacle, we have eliminations left, and not visited with reduced count
56                        else if (grid[nextRow][nextCol] == 1 && remainingEliminations > 0 
57                                && !visited[nextRow][nextCol][remainingEliminations - 1]) {
58                            queue.offer(new int[] {nextRow, nextCol, remainingEliminations - 1});
59                            visited[nextRow][nextCol][remainingEliminations - 1] = true;
60                        }
61                    }
62                }
63            }
64        }
65      
66        // No path found to destination
67        return -1;
68    }
69}
70
1class Solution {
2public:
3    int shortestPath(vector<vector<int>>& grid, int k) {
4        int rows = grid.size();
5        int cols = grid[0].size();
6      
7        // Early optimization: if we have enough eliminations to go straight through
8        // The maximum obstacles we'd encounter going right then down (or down then right)
9        // is (rows - 1) + (cols - 1) - 1 = rows + cols - 3
10        if (k >= rows + cols - 3) {
11            return rows + cols - 2;
12        }
13      
14        // BFS queue storing {row, col, remaining_eliminations}
15        queue<vector<int>> bfsQueue;
16        bfsQueue.push({0, 0, k});
17      
18        // 3D visited array: visited[row][col][remaining_k]
19        // Tracks if we've visited cell (row, col) with exactly remaining_k eliminations left
20        vector<vector<vector<bool>>> visited(rows, 
21            vector<vector<bool>>(cols, vector<bool>(k + 1, false)));
22        visited[0][0][k] = true;
23      
24        // Track the current path length
25        int pathLength = 0;
26      
27        // Direction vectors for moving up, right, down, left
28        vector<int> directions = {-1, 0, 1, 0, -1};
29      
30        // BFS to find shortest path
31        while (!bfsQueue.empty()) {
32            pathLength++;
33          
34            // Process all nodes at current level
35            int levelSize = bfsQueue.size();
36            for (int i = 0; i < levelSize; i++) {
37                vector<int> current = bfsQueue.front();
38                bfsQueue.pop();
39              
40                int currentRow = current[0];
41                int currentCol = current[1];
42                int remainingK = current[2];
43              
44                // Try all four directions
45                for (int dir = 0; dir < 4; dir++) {
46                    int nextRow = currentRow + directions[dir];
47                    int nextCol = currentCol + directions[dir + 1];
48                  
49                    // Check if next position is within grid bounds
50                    if (nextRow >= 0 && nextRow < rows && 
51                        nextCol >= 0 && nextCol < cols) {
52                      
53                        // Check if we reached the destination
54                        if (nextRow == rows - 1 && nextCol == cols - 1) {
55                            return pathLength;
56                        }
57                      
58                        // If next cell is empty and not visited with current k
59                        if (grid[nextRow][nextCol] == 0 && 
60                            !visited[nextRow][nextCol][remainingK]) {
61                            bfsQueue.push({nextRow, nextCol, remainingK});
62                            visited[nextRow][nextCol][remainingK] = true;
63                        }
64                        // If next cell is obstacle, we have eliminations left, 
65                        // and haven't visited with k-1 eliminations
66                        else if (grid[nextRow][nextCol] == 1 && 
67                                 remainingK > 0 && 
68                                 !visited[nextRow][nextCol][remainingK - 1]) {
69                            bfsQueue.push({nextRow, nextCol, remainingK - 1});
70                            visited[nextRow][nextCol][remainingK - 1] = true;
71                        }
72                    }
73                }
74            }
75        }
76      
77        // No path found
78        return -1;
79    }
80};
81
1/**
2 * Finds the shortest path in a grid with ability to remove up to k obstacles
3 * @param grid - 2D array where 0 represents empty cell and 1 represents obstacle
4 * @param k - Maximum number of obstacles that can be removed
5 * @returns Minimum number of steps to reach bottom-right corner, or -1 if impossible
6 */
7function shortestPath(grid: number[][], k: number): number {
8    const rows = grid.length;
9    const cols = grid[0].length;
10  
11    // Optimization: If we can remove enough obstacles, we can go straight (Manhattan distance)
12    if (k >= rows + cols - 3) {
13        return rows + cols - 2;
14    }
15
16    // BFS queue: [row, col, remaining obstacle removals]
17    let queue: Point[] = [[0, 0, k]];
18  
19    // 3D visited array: visited[row][col][remainingK] tracks if we've visited this state
20    const visited = Array.from({ length: rows }, () =>
21        Array.from({ length: cols }, () => 
22            Array.from({ length: k + 1 }, () => false)
23        )
24    );
25    visited[0][0][k] = true;
26  
27    // Direction vectors for moving up, right, down, left
28    const directions = [0, 1, 0, -1, 0];
29    let steps = 0;
30
31    // BFS to find shortest path
32    while (queue.length > 0) {
33        const nextQueue: Point[] = [];
34        steps++;
35
36        // Process all cells at current distance
37        for (const [currentRow, currentCol, remainingK] of queue) {
38            // Try all 4 directions
39            for (let dir = 0; dir < 4; dir++) {
40                const nextRow = currentRow + directions[dir];
41                const nextCol = currentCol + directions[dir + 1];
42              
43                // Check if we reached the destination
44                if (nextRow === rows - 1 && nextCol === cols - 1) {
45                    return steps;
46                }
47              
48                // Get cell value (undefined if out of bounds)
49                const cellValue = grid[nextRow]?.[nextCol];
50              
51                // If cell is empty and not visited with current k value
52                if (cellValue === 0 && !visited[nextRow][nextCol][remainingK]) {
53                    nextQueue.push([nextRow, nextCol, remainingK]);
54                    visited[nextRow][nextCol][remainingK] = true;
55                } 
56                // If cell is obstacle, we have removals left, and not visited with k-1
57                else if (cellValue === 1 && remainingK > 0 && !visited[nextRow][nextCol][remainingK - 1]) {
58                    nextQueue.push([nextRow, nextCol, remainingK - 1]);
59                    visited[nextRow][nextCol][remainingK - 1] = true;
60                }
61            }
62        }
63      
64        queue = nextQueue;
65    }
66  
67    // No path found
68    return -1;
69}
70
71// Type definition for a point in the grid with remaining obstacle removals
72type Point = [number, number, number];
73

Time and Space Complexity

Time Complexity: O(m * n * k)

The algorithm uses BFS to explore the grid. In the worst case, we need to visit each cell (x, y) with different remaining obstacle elimination counts from 0 to k. Since each state is represented as (row, col, remaining_k), there are at most m * n * (k + 1) unique states to explore. Each state is processed once due to the visited set check, and for each state, we examine 4 directions which is O(1). Therefore, the overall time complexity is O(m * n * k).

Space Complexity: O(m * n * k)

The space complexity is dominated by:

  • The vis set which stores visited states as tuples (row, col, remaining_k). In the worst case, this can contain up to m * n * (k + 1) states, giving O(m * n * k) space.
  • The queue q which in the worst case can also hold O(m * n * k) states during BFS traversal.
  • Other variables use O(1) space.

The early termination optimization if k >= m + n - 3: return m + n - 2 helps when k is large enough to eliminate all obstacles in any Manhattan distance path, but doesn't change the worst-case complexity analysis.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect State Representation in Visited Set

Pitfall: A common mistake is tracking visited cells using only 2D coordinates (row, col) instead of the 3D state (row, col, remaining_eliminations).

# WRONG: This will miss valid paths
visited = set()
visited.add((row, col))  # Missing elimination count!

Why it's wrong: The same cell can be visited multiple times with different elimination counts remaining. For example, reaching cell (2, 3) with 5 eliminations left is different from reaching it with 2 eliminations left. The path with more eliminations remaining might lead to a successful route while the other might not.

Solution:

# CORRECT: Track the complete state
visited = {(0, 0, k)}  # Include elimination count
state = (next_row, next_col, remaining_k)
if state not in visited:
    visited.add(state)

2. Off-by-One Error in Optimization Check

Pitfall: Incorrectly calculating the maximum obstacles on the shortest Manhattan path.

# WRONG: Off by one
if k >= rows + cols - 2:  # This allows too many cases to skip BFS
    return rows + cols - 2

Why it's wrong: The shortest path has rows + cols - 2 steps total. The maximum obstacles on this path is rows + cols - 3 (excluding the guaranteed empty start and end cells). Using rows + cols - 2 would incorrectly assume we might need to eliminate the destination cell itself.

Solution:

# CORRECT: Maximum obstacles is total steps minus 1
if k >= rows + cols - 3:
    return rows + cols - 2

3. Returning Steps Before Incrementing

Pitfall: Forgetting that BFS starts at distance 0, not 1.

# WRONG: Returns one less than actual distance
steps = 0
while queue:
    for _ in range(len(queue)):
        # ... process current level
        if next_row == rows - 1 and next_col == cols - 1:
            return steps  # Should be steps + 1!
    steps += 1

Solution: Either increment steps at the beginning of each level iteration (as shown in the correct code), or return steps + 1 when the destination is found.

4. Not Handling the Starting Cell Correctly

Pitfall: Assuming the starting cell (0, 0) is always empty.

# WRONG: Doesn't check if start is an obstacle
if grid[0][0] == 1:
    # Need to handle this case!

Why it matters: If the starting cell contains an obstacle, you need to use one elimination immediately, starting with k-1 eliminations instead of k.

Solution:

# CORRECT: Handle starting cell obstacle
initial_k = k if grid[0][0] == 0 else k - 1
if initial_k < 0:
    return -1  # Can't even start
queue = deque([(0, 0, initial_k)])
visited = {(0, 0, initial_k)}

5. Edge Case: Single Cell Grid

Pitfall: Not handling the case where m = 1 and n = 1.

Solution: Add a check at the beginning:

if rows == 1 and cols == 1:
    return 0  # Already at destination
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

The three-steps of Depth First Search are:

  1. Identify states;
  2. Draw the state-space tree;
  3. DFS on the state-space tree.

Recommended Readings

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

Load More