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 contain1
, effectively treating them as0
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.
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:
-
Level-by-level exploration: BFS explores all cells at distance
d
before moving to distanced+1
, guaranteeing the first time we reach the destination is via the shortest path. -
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. -
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)
withk
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 EvaluatorExample 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:
- We tracked states as (row, col, remaining_eliminations), not just (row, col)
- Using our one elimination wisely at (0,1) opened up a path
- BFS guaranteed we found the shortest path first
- 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 tom * n * (k + 1)
states, givingO(m * n * k)
space. - The queue
q
which in the worst case can also holdO(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
The three-steps of Depth First Search are:
- Identify states;
- Draw the state-space tree;
- DFS on the state-space tree.
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!