Shortest Path in a Grid with Obstacles Elimination

Hard BFS

Problem

Given a grid of 0s and 1s and an integer k, return the minimum number of steps to walk from the top-left corner to the bottom-right corner, where you can eliminate at most k obstacles (cells with 1). If it is not possible, return -1.

  • m == grid.length
  • n == grid[i].length
  • 1 ≤ m, n ≤ 40
  • 0 ≤ k ≤ m * n
  • grid[i][j] is 0 or 1

Example

Input: grid = [[0,1,1],[1,1,0],[1,0,0]], k = 1
Output: 4

Starting at (0,0), the algorithm explores neighbors using BFS while tracking remaining obstacle eliminations. It can eliminate one obstacle to move through (0,1) or (1,0). The shortest path with at most one elimination is (0,0) -> (0,1) [eliminate obstacle] -> (0,2) -> (1,2) -> (2,2), totaling 4 steps. The BFS ensures the first time the destination is reached with a valid elimination count, the path length is minimal.

Approach

Straightforward Solution

A naive BFS ignoring obstacle elimination would fail because it cannot pass through obstacles. A brute-force approach trying all paths with obstacle eliminations would be exponential and infeasible.

Core Observation

The problem requires finding the shortest path in a grid with obstacles, but with the added complexity that up to k obstacles can be eliminated. This means the state is not just the position but also how many eliminations remain, making it a multi-dimensional shortest path problem.

Path to Optimal

The key insight is to augment the BFS state with the remaining number of obstacle eliminations. This transforms the problem into a shortest path on a graph where nodes are (row, col, remaining eliminations). By tracking visited states with this triple, the algorithm avoids revisiting states that cannot yield a shorter path. This ensures BFS explores all viable paths efficiently.

Optimal Approach

Use BFS starting from (0,0) with k eliminations available. Each step explores neighbors, decrementing eliminations when passing through obstacles. Maintain a visited set of (row, col, remaining eliminations) to prevent redundant exploration. The first time the destination is dequeued, return the number of steps taken. If the queue empties without reaching the destination, return -1.

Time

O(m * n * k)

Each cell can be visited with up to k+1 different remaining elimination states. BFS explores each unique state once, resulting in O(m * n * k) time.

Space

O(m * n * k)

The visited set and queue can hold up to O(m * n * k) states, as each position can be reached with different remaining eliminations.

Pattern Spotlight

BFS (Shortest Path with State Augmentation)

When shortest path problems involve additional constraints or resources (like obstacle eliminations), augment the BFS state to include these parameters, and track visited states accordingly to avoid redundant or invalid paths.

Solution

1from collections import deque
2
3class Solution:
4 def shortestPath(self, grid: List[List[int]], k: int) -> int:
5 def valid(row, col):
6 return 0 <= row < m and 0 <= col < n
7
8 m = len(grid)
9 n = len(grid[0])
10 queue = deque([(0, 0, k, 0)])
11 seen = {(0, 0, k)}
12 directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
13
14 while queue:
15 row, col, remain, steps = queue.popleft()
16 if row == m - 1 and col == n - 1:
17 return steps
18
19 for dx, dy in directions:
20 nr, nc = row + dy, col + dx
21 if valid(nr, nc):
22 if grid[nr][nc] == 0:
23 if (nr, nc, remain) not in seen:
24 seen.add((nr, nc, remain))
25 queue.append((nr, nc, remain, steps + 1))
26 elif remain and (nr, nc, remain - 1) not in seen:
27 seen.add((nr, nc, remain - 1))
28 queue.append((nr, nc, remain - 1, steps + 1))
29
30 return -1

Step-by-Step Solution

1

Define a Bounds-Checking Helper for Grid Coordinates

5def valid(row, col):
6 return 0 <= row < m and 0 <= col < n

Objective

To centralize the grid boundary check so neighbor exploration code stays clean, readable, and correct.

Key Insight

By isolating the bounds logic in a helper like valid(row, col), every place that needs to test a coordinate can reuse the same condition. This reduces duplication and the risk of subtle off-by-one or swapped-index bugs in the BFS loop.

Interview Quick-Check

Core Logic

The helper abstracts the row/column bounds check so the BFS body focuses on high-level state transitions instead of low-level index conditions.

State & Boundaries

The helper enforces that 0 <= row < m and 0 <= col < n, which defines the valid region of the grid.

Common Pitfalls & Bugs

Implementing the bounds check inline in multiple places or mixing up row/col ranges is a common source of index errors and incorrect traversals.

2

Initialize BFS Queue, Visited Set, and Movement Directions

8m = len(grid)
9n = len(grid[0])
10queue = deque([(0, 0, k, 0)])
11seen = {(0, 0, k)}
12directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]

Objective

To set up the initial BFS state with starting position, remaining eliminations, and data structures for traversal and visited tracking.

Key Insight

Representing each BFS node as a tuple (row, col, remaining eliminations, steps) allows the algorithm to track both position and resource state. The visited set stores triples (row, col, remaining eliminations) to prevent revisiting states that cannot improve the solution. Defining movement directions upfront simplifies neighbor exploration.

Interview Quick-Check

Core Logic

The BFS queue stores states including remaining eliminations, enabling exploration of paths with different obstacle usage.

State & Boundaries

The visited set must include remaining eliminations to distinguish states that differ only in resource availability.

Common Pitfalls & Bugs

Failing to track remaining eliminations in visited leads to revisiting states and exponential blowup.

3

Perform BFS Traversal with State-Aware Neighbor Exploration

14while queue:
15 row, col, remain, steps = queue.popleft()
16 if row == m - 1 and col == n - 1:
17 return steps
19 for dx, dy in directions:
20 nr, nc = row + dy, col + dx
21 if valid(nr, nc):
22 if grid[nr][nc] == 0:
23 if (nr, nc, remain) not in seen:
24 seen.add((nr, nc, remain))
25 queue.append((nr, nc, remain, steps + 1))
26 elif remain and (nr, nc, remain - 1) not in seen:
27 seen.add((nr, nc, remain - 1))
28 queue.append((nr, nc, remain - 1, steps + 1))

Objective

To explore the grid layer by layer, updating steps and remaining eliminations, and enqueueing valid next states.

Key Insight

At each step, the algorithm dequeues a state and checks if it reached the destination. It then explores neighbors, validating boundaries and obstacle presence. If the neighbor is empty, it proceeds with the same remaining eliminations. If the neighbor is an obstacle and eliminations remain, it proceeds with one fewer elimination. This state-aware BFS ensures all shortest paths considering obstacle removals are explored efficiently.

Interview Quick-Check

Core Logic

The BFS expands states by considering obstacle elimination usage, ensuring shortest path discovery with resource constraints.

State & Boundaries

Neighbors are only enqueued if within grid bounds and if the resulting state has not been visited.

Common Pitfalls & Bugs

Incorrectly swapping dx and dy or miscalculating neighbor coordinates leads to wrong traversal.

Complexity

Each unique state is enqueued once, bounding the BFS complexity to O(m * n * k).

4

Return -1 if Destination is Unreachable

30return -1

Objective

To signal failure by returning -1 when BFS exhausts all states without reaching the target.

Key Insight

If the BFS queue empties without dequeuing the destination, no valid path exists within the given obstacle elimination constraints. Returning -1 correctly communicates this to the caller.

Interview Quick-Check

Core Logic

Returning -1 after BFS completion indicates no feasible path was found.

Line Analysis

This solution has 4 Critical lines interviewers watch for.

Line 16 Critical
if row == m - 1 and col == n - 1:

Check if the current position is the destination.

Because BFS explores states in order of increasing steps, the first time the destination is dequeued guarantees the shortest path, making immediate return correct and optimal.

Line 11 Critical
seen = {(0, 0, k)}

Initialize the visited set with the starting state to prevent revisiting.

Tracking visited states including remaining eliminations prevents redundant exploration and infinite loops, ensuring BFS efficiency.

Line 21 Critical
if valid(nr, nc):

Check if the neighbor is within grid boundaries.

Boundary validation prevents invalid memory access and ensures only valid cells are processed.

Line 23 Critical
if (nr, nc, remain) not in seen:

Check if the neighbor state with the same remaining eliminations has not been visited.

Avoiding revisiting states with identical resource availability prevents redundant work and infinite loops.

Line 6 Core
return 0 <= row < m and 0 <= col < n

Return a boolean indicating whether row and col satisfy 0 <= row < m and 0 <= col < n.

The chained inequalities implement the exact boundary condition for valid grid indices, preventing out-of-bounds access and ensuring only valid neighbors are processed.

Line 10 Core
queue = deque([(0, 0, k, 0)])

Initialize the BFS queue with the starting position, full obstacle eliminations, and zero steps taken.

This tuple represents the initial BFS state, capturing position, remaining eliminations, and path length, which is essential for state-aware traversal.

Line 17 Core
return steps

Return the number of steps taken to reach the destination.

Returning here terminates the search with the minimal path length found by BFS.

Line 24 Core
seen.add((nr, nc, remain))

Mark the neighbor state as visited with the current remaining eliminations.

Recording visited states ensures BFS does not revisit the same state, maintaining efficiency and correctness.

Line 25 Core
queue.append((nr, nc, remain, steps + 1))

Enqueue the neighbor state with unchanged remaining eliminations and incremented steps.

Adding the neighbor to the BFS queue continues the search with updated path length while preserving obstacle elimination count.

Line 26 Core
elif remain and (nr, nc, remain - 1) not in seen:

Check if the neighbor cell is an obstacle and if eliminations remain.

This condition determines if the algorithm can use an elimination to pass through an obstacle.

Line 27 Core
seen.add((nr, nc, remain - 1))

Mark the neighbor state as visited with one fewer remaining elimination.

Tracking visited states with updated eliminations prevents revisiting less optimal paths and ensures correctness.

Line 28 Core
queue.append((nr, nc, remain - 1, steps + 1))

Enqueue the neighbor state with decremented remaining eliminations and incremented steps.

This enqueues a valid path that uses one elimination to pass through an obstacle, enabling exploration of all feasible paths.

Line 15 Core
row, col, remain, steps = queue.popleft()

Dequeue the next state to process, unpacking position, remaining eliminations, and steps.

Extracting the current BFS state is necessary to explore neighbors and update path length.

Line 22 Core
if grid[nr][nc] == 0:

Check if the neighbor cell is empty (no obstacle).

Distinguishing empty cells allows the algorithm to proceed without using an elimination.

Line 30 Core
return -1

Return -1 if BFS completes without reaching the destination.

Returning -1 correctly signals that no valid path exists within the given obstacle elimination constraints.

Line 5 Standard
def valid(row, col):

Define a helper function that determines whether a given (row, col) coordinate lies inside the grid.

Centralizing the bounds check in a helper function keeps the BFS neighbor-exploration code simple and reduces the chance of duplicating or miswriting the boundary logic.

Line 14 Standard
while queue:

Begin BFS loop that continues until the queue is empty.

The while loop drives the BFS traversal, processing states layer by layer to guarantee shortest path discovery.

Line 20 Standard
nr, nc = row + dy, col + dx

Calculate the neighbor's row and column indices.

Computing neighbor coordinates is essential for boundary and obstacle checks.

Line 19 Standard
for dx, dy in directions:

Iterate over all four movement directions to explore neighbors.

Exploring all adjacent cells ensures BFS covers the entire reachable grid space.

Line 12 Standard
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]

Define the four possible movement directions (right, down, left, up).

Predefining directions simplifies neighbor exploration and reduces code duplication.

Line 8 Basic
m = len(grid)

Store the number of rows in the grid.

Caching the grid height improves readability and avoids repeated calls to len(grid).

Line 9 Basic
n = len(grid[0])

Store the number of columns in the grid.

Caching the grid width improves readability and avoids repeated calls to len(grid[0]).

Test Your Understanding

Why must the BFS state include the remaining obstacle eliminations, and why is it insufficient to track only positions?

Because the same position can be reached with different numbers of remaining eliminations, which affects future path possibilities. Tracking only positions would cause incorrect pruning and potentially miss shorter paths that use eliminations differently.

Related Problems

BFS pattern

Don't just read it. Drill it.

Reconstruct Shortest Path in a Grid with Obstacles Elimination from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Shortest Path in a Grid with Obstacles Elimination drill

or drill a free problem