LeetCode 1293 - Shortest Path in a Grid with Obstacles Elimination
This problem asks us to find the minimum number of steps required to move from the top-left corner of a grid to the bott
Difficulty: 🔴 Hard
Topics: Array, Breadth-First Search, Matrix
Solution
Problem Understanding
This problem asks us to find the minimum number of steps required to move from the top-left corner of a grid to the bottom-right corner. The grid contains two kinds of cells:
0, representing an empty cell that can be entered freely1, representing an obstacle
Normally, obstacles block movement. However, we are allowed to eliminate up to k obstacles during the journey. Eliminating an obstacle means we may move through that blocked cell while consuming one unit from our obstacle elimination budget.
Movement is restricted to the four cardinal directions:
- up
- down
- left
- right
Each move costs exactly one step. The goal is to minimize the total number of steps needed to reach the destination.
The input consists of:
grid, anm x nmatrixk, the maximum number of obstacles we may remove
The output is:
- the minimum number of steps required to reach
(m - 1, n - 1) -1if no valid path exists
The constraints are important because they guide us toward the appropriate algorithmic strategy.
m, n <= 40- therefore, the grid contains at most
1600cells kmay be as large asm * n
A brute-force search over all possible paths would explode combinatorially and become infeasible. However, the grid size is small enough that a graph traversal algorithm with carefully managed state can work efficiently.
One subtle but critical detail is that reaching the same cell with different remaining eliminations represents different states. For example:
- arriving at
(2,3)with5eliminations left is strictly better than arriving there with1elimination left - therefore, position alone is not enough to represent visited states
There are also several edge cases worth noting upfront:
- The grid may already be a single cell, meaning the answer is
0. - A shortest geometric path may be impossible unless obstacles are removed.
- Multiple paths may reach the same cell with different elimination counts.
- If
kis very large, we can effectively ignore many obstacles and take a nearly direct shortest route.
Approaches
Brute Force Approach
A naive solution would attempt to explore every possible path from the start to the destination using Depth-First Search.
At every step, we would:
- move in four directions
- optionally eliminate an obstacle if encountered
- continue recursively until reaching the destination
This approach is correct because it eventually explores every valid path. Among all successful paths, we could track the minimum path length.
However, the number of possible paths grows exponentially. Even on relatively small grids, the search space becomes enormous because:
- paths can revisit cells
- the same cell may be reached with different remaining eliminations
- many branches lead to dead ends
Without aggressive pruning, this approach becomes computationally infeasible.
Optimal Approach, Breadth-First Search with State Tracking
The key observation is that this is fundamentally a shortest path problem on an unweighted graph.
Each move costs exactly one step, which makes Breadth-First Search ideal because BFS guarantees that the first time we reach the target, we have used the minimum number of steps.
The challenge is that the state is not just the current position.
The full state consists of:
- current row
- current column
- remaining obstacle eliminations
Why is this necessary?
Suppose we reach the same cell twice:
- once with
0eliminations remaining - once with
3eliminations remaining
These are not equivalent states. The second state is strictly more powerful because it allows future obstacles to be removed.
Therefore, we cannot use a simple visited[row][col] structure.
Instead, we store the maximum remaining eliminations seen for each cell. If we revisit a cell with fewer remaining eliminations than before, that state is dominated and can be skipped.
This optimization dramatically reduces redundant exploration.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | Exponential | Exponential | Explores all possible paths recursively |
| Optimal BFS | O(m * n * k) | O(m * n * k) | BFS over augmented state space |
Algorithm Walkthrough
- Initialize dimensions and handle trivial cases.
If the grid contains only one cell, we are already at the destination, so the answer is 0.
2. Apply an important optimization.
The shortest possible path in an m x n grid without detours is:
m + n - 2
If k is at least this value, then we can always walk directly toward the destination while removing any obstacles encountered.
In that case, we immediately return:
m + n - 2
3. Create the BFS queue.
Each queue entry stores:
- current row
- current column
- remaining eliminations
- steps taken so far
We begin with:
- position
(0,0) keliminations remaining0steps
- Track visited states efficiently.
Instead of storing every (row, col, remaining_k) combination separately, we maintain:
visited[row][col] = maximum remaining eliminations seen
This works because reaching the same cell with fewer remaining eliminations can never be better. 5. Perform BFS level traversal.
Repeatedly pop states from the queue.
For each state:
- try all four directions
- skip out-of-bounds cells
- determine whether entering the next cell consumes an elimination
- Update remaining eliminations.
If the next cell contains an obstacle:
- subtract one from remaining eliminations
If remaining eliminations become negative:
- this move is invalid
- Check for destination.
If the next cell is the bottom-right corner:
- return
steps + 1
Because BFS explores states in increasing order of distance, this is guaranteed to be the shortest path. 8. Prune dominated states.
Before pushing a new state into the queue:
- compare its remaining eliminations against the best previously recorded value for that cell
If we already reached the cell with an equal or larger remaining budget:
- skip this state
Otherwise:
- update
visited - enqueue the new state
- If BFS finishes without reaching the target:
return -1
Why it works
Breadth-First Search explores states in order of increasing path length. Therefore, the first time the destination is reached must correspond to the minimum number of steps.
The pruning rule is also correct because a state with more remaining eliminations strictly dominates one with fewer eliminations at the same position. Any future path available from the weaker state is also available from the stronger state.
Together, these properties guarantee both correctness and efficiency.
Python Solution
from collections import deque
from typing import List
class Solution:
def shortestPath(self, grid: List[List[int]], k: int) -> int:
rows = len(grid)
cols = len(grid[0])
if rows == 1 and cols == 1:
return 0
# If we can eliminate enough obstacles to walk directly
# to the destination, return the Manhattan distance.
if k >= rows + cols - 2:
return rows + cols - 2
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
queue = deque([(0, 0, k, 0)])
# visited[row][col] stores the maximum remaining
# eliminations seen at this cell.
visited = [[-1] * cols for _ in range(rows)]
visited[0][0] = k
while queue:
row, col, remaining_k, steps = queue.popleft()
for dr, dc in directions:
new_row = row + dr
new_col = col + dc
if (
new_row < 0
or new_row >= rows
or new_col < 0
or new_col >= cols
):
continue
new_remaining_k = remaining_k - grid[new_row][new_col]
if new_remaining_k < 0:
continue
if new_row == rows - 1 and new_col == cols - 1:
return steps + 1
# Skip dominated states.
if visited[new_row][new_col] >= new_remaining_k:
continue
visited[new_row][new_col] = new_remaining_k
queue.append(
(
new_row,
new_col,
new_remaining_k,
steps + 1,
)
)
return -1
The implementation begins by extracting the grid dimensions and handling the single-cell case. This avoids unnecessary BFS work when the start is already the destination.
The next optimization checks whether k is large enough to ignore all obstacles along a direct shortest route. Since the shortest geometric distance is always rows + cols - 2, having at least that many eliminations guarantees success along a direct path.
The BFS queue stores complete traversal state:
- current coordinates
- remaining eliminations
- current path length
A deque is used because BFS repeatedly removes elements from the front and appends new states to the back in constant time.
The visited matrix stores the best remaining elimination count observed for each cell. This is the key optimization that prevents redundant exploration.
During traversal, each neighbor is examined:
- invalid coordinates are skipped
- obstacles consume one elimination
- states with negative remaining eliminations are discarded
If the destination is reached, the algorithm immediately returns the current distance plus one step.
Before enqueuing a new state, the algorithm checks whether the same cell has already been reached with an equal or better remaining elimination count. If so, the new state is dominated and ignored.
If BFS exhausts all states without finding the destination, the function returns -1.
Go Solution
package main
import "container/list"
type State struct {
row int
col int
remainingK int
steps int
}
func shortestPath(grid [][]int, k int) int {
rows := len(grid)
cols := len(grid[0])
if rows == 1 && cols == 1 {
return 0
}
if k >= rows+cols-2 {
return rows + cols - 2
}
directions := [][]int{
{1, 0},
{-1, 0},
{0, 1},
{0, -1},
}
queue := list.New()
queue.PushBack(State{0, 0, k, 0})
visited := make([][]int, rows)
for i := 0; i < rows; i++ {
visited[i] = make([]int, cols)
for j := 0; j < cols; j++ {
visited[i][j] = -1
}
}
visited[0][0] = k
for queue.Len() > 0 {
front := queue.Front()
queue.Remove(front)
current := front.Value.(State)
for _, dir := range directions {
newRow := current.row + dir[0]
newCol := current.col + dir[1]
if newRow < 0 || newRow >= rows ||
newCol < 0 || newCol >= cols {
continue
}
newRemainingK := current.remainingK - grid[newRow][newCol]
if newRemainingK < 0 {
continue
}
if newRow == rows-1 && newCol == cols-1 {
return current.steps + 1
}
if visited[newRow][newCol] >= newRemainingK {
continue
}
visited[newRow][newCol] = newRemainingK
queue.PushBack(
State{
newRow,
newCol,
newRemainingK,
current.steps + 1,
},
)
}
}
return -1
}
The Go solution follows the same algorithmic structure as the Python implementation.
A custom State struct is used to store BFS state cleanly. The BFS queue is implemented using Go's container/list package because it provides efficient front removal and back insertion.
Unlike Python, Go does not provide dynamic tuple structures, so explicit struct fields improve readability and type safety.
The visited matrix is initialized manually with -1 values because Go slices default to 0, which would incorrectly imply that cells had already been visited with zero eliminations remaining.
No integer overflow concerns exist because all values remain small under the problem constraints.
Worked Examples
Example 1
grid =
[
[0,0,0],
[1,1,0],
[0,0,0],
[0,1,1],
[0,0,0]
]
k = 1
Target cell is (4,2).
Initial queue:
| Queue State | Meaning |
|---|---|
(0,0,1,0) |
row=0, col=0, remaining_k=1, steps=0 |
Initial visited:
| Cell | Best Remaining k |
|---|---|
(0,0) |
1 |
Step 1
Pop (0,0,1,0).
Possible moves:
| Next Cell | Obstacle? | New k | Valid? |
|---|---|---|---|
(1,0) |
Yes | 0 | Yes |
(0,1) |
No | 1 | Yes |
Queue becomes:
| Queue |
|---|
(1,0,0,1) |
(0,1,1,1) |
Step 2
Pop (1,0,0,1).
Possible moves:
| Next Cell | Obstacle? | New k | Valid? |
|---|---|---|---|
(2,0) |
No | 0 | Yes |
(1,1) |
Yes | -1 | No |
Queue:
| Queue |
|---|
(0,1,1,1) |
(2,0,0,2) |
Step 3
Pop (0,1,1,1).
Possible moves:
| Next Cell | Obstacle? | New k |
|---|---|---|
(0,2) |
No | 1 |
(1,1) |
Yes | 0 |
Queue expands further.
Eventually BFS reaches:
(4,2)
after 6 steps.
Returned answer:
6
Example 2
grid =
[
[0,1,1],
[1,1,1],
[1,0,0]
]
k = 1
The destination cannot be reached because every valid route requires eliminating at least two obstacles.
BFS explores all reachable states and eventually exhausts the queue.
Returned answer:
-1
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(m * n * k) | Each state may be processed once |
| Space | O(m * n * k) | BFS queue and state tracking |
The algorithm explores states defined by:
(row, col, remaining_k)
There are at most:
m * n * (k + 1)
distinct states.
For each state, we attempt four directional moves, which is constant work. Therefore, total runtime is proportional to the number of reachable states.
The queue and visited tracking together also require storage proportional to the total state space.
Test Cases
from typing import List
class Solution:
def shortestPath(self, grid: List[List[int]], k: int) -> int:
from collections import deque
rows = len(grid)
cols = len(grid[0])
if rows == 1 and cols == 1:
return 0
if k >= rows + cols - 2:
return rows + cols - 2
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
queue = deque([(0, 0, k, 0)])
visited = [[-1] * cols for _ in range(rows)]
visited[0][0] = k
while queue:
row, col, remaining_k, steps = queue.popleft()
for dr, dc in directions:
nr = row + dr
nc = col + dc
if nr < 0 or nr >= rows or nc < 0 or nc >= cols:
continue
new_k = remaining_k - grid[nr][nc]
if new_k < 0:
continue
if nr == rows - 1 and nc == cols - 1:
return steps + 1
if visited[nr][nc] >= new_k:
continue
visited[nr][nc] = new_k
queue.append((nr, nc, new_k, steps + 1))
return -1
solution = Solution()
# Example 1
assert solution.shortestPath(
[
[0,0,0],
[1,1,0],
[0,0,0],
[0,1,1],
[0,0,0]
],
1
) == 6 # standard example with one elimination
# Example 2
assert solution.shortestPath(
[
[0,1,1],
[1,1,1],
[1,0,0]
],
1
) == -1 # impossible with insufficient eliminations
# Single cell grid
assert solution.shortestPath([[0]], 1) == 0 # already at destination
# No obstacles
assert solution.shortestPath(
[
[0,0],
[0,0]
],
0
) == 2 # direct shortest path
# Must eliminate exactly one obstacle
assert solution.shortestPath(
[
[0,1],
[1,0]
],
1
) == 2 # elimination required
# Large k enables direct path
assert solution.shortestPath(
[
[0,1,1],
[1,1,0],
[0,0,0]
],
10
) == 4 # k large enough for Manhattan path
# Path exists without eliminations
assert solution.shortestPath(
[
[0,0,0],
[1,1,0],
[0,0,0]
],
0
) == 4 # normal BFS shortest route
# Complex revisiting scenario
assert solution.shortestPath(
[
[0,1,0],
[0,1,0],
[0,0,0]
],
1
) == 4 # revisiting with better remaining k matters
# Impossible even with some eliminations
assert solution.shortestPath(
[
[0,1,1],
[1,1,1],
[1,1,0]
],
2
) == -1 # still blocked
print("All test cases passed!")
| Test | Why |
|---|---|
| Example 1 | Validates standard obstacle elimination behavior |
| Example 2 | Confirms impossible paths return -1 |
| Single cell grid | Tests trivial base case |
| No obstacles | Ensures ordinary BFS behavior works |
| Exact elimination needed | Verifies obstacle removal logic |
| Large k | Tests optimization for direct Manhattan path |
| Path without eliminations | Confirms algorithm works with k = 0 |
| Revisiting scenario | Validates state dominance pruning |
| Impossible dense grid | Ensures exhaustive BFS termination |
Edge Cases
One important edge case is a single-cell grid such as [[0]]. In this scenario, the start and destination are the same cell. A buggy implementation might incorrectly perform BFS and return 1 instead of 0. The implementation handles this immediately with an early return before any traversal begins.
Another subtle case occurs when the same cell is reached multiple times with different remaining elimination counts. A naive visited structure based only on coordinates would incorrectly prune potentially optimal states. For example, reaching a cell later with more remaining eliminations may enable a shorter future route. The implementation solves this by storing the maximum remaining eliminations observed for each cell.
A third important edge case happens when k is extremely large relative to the grid dimensions. Without optimization, BFS would still explore a massive state space unnecessarily. The implementation recognizes that if k >= rows + cols - 2, we can always move directly to the destination while eliminating obstacles along the way. In that case, the answer is simply the Manhattan distance.
Another tricky case involves impossible grids where all routes require more eliminations than available. A flawed implementation might loop indefinitely or incorrectly assume every grid is solvable. This solution naturally handles such cases because BFS eventually exhausts all reachable states and returns -1.
Finally, boundary movement is a common source of bugs in grid problems. Every neighbor expansion must verify row and column limits before accessing the grid. The implementation explicitly checks bounds before processing neighbors, preventing invalid memory access and incorrect traversal.