2290. Minimum Obstacle Removal to Reach Corner
Problem Description
You have a 2D grid of size m x n
where each cell contains either:
0
: an empty cell you can freely move through1
: an obstacle that blocks your path (but can be removed)
Starting from the top-left corner (0, 0)
, you need to reach the bottom-right corner (m-1, n-1)
. You can move in four directions: up, down, left, or right, but only to empty cells.
The task is to find the minimum number of obstacles you need to remove to create a valid path from start to finish.
For example, if you have a grid where some cells are blocked by obstacles (1s), you might need to remove some of these obstacles to make a path possible. The goal is to minimize how many obstacles you remove.
Key points:
- You start at position
(0, 0)
- You want to reach position
(m-1, n-1)
- You can only move to adjacent cells (up, down, left, right)
- You can only move through empty cells (0s) unless you remove an obstacle
- Each obstacle removal has a cost of 1
- Find the minimum total cost (minimum obstacles to remove)
This is essentially finding the path with the least "cost" where moving through an empty cell costs 0 and moving through an obstacle costs 1.
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 find a path from one node to another.
Is it a tree?
- No: The grid structure allows multiple paths between nodes and can have cycles, so it's not a tree.
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 path with the minimum cost (minimum obstacles to remove) from the start to the end position.
Is the graph Weighted?
- Yes: The graph has weighted edges - moving to an empty cell (0) has weight 0, and moving to an obstacle (1) has weight 1.
Dijkstra's Algorithm?
- While Dijkstra's could work here, the special property of having only weights 0 and 1 allows us to use a more efficient 0-1 BFS approach.
Conclusion: The flowchart leads us to consider this as a weighted shortest path problem. However, since we only have edge weights of 0 and 1, we can optimize using BFS with a double-ended queue (0-1 BFS) instead of full Dijkstra's algorithm. This is why the solution uses BFS - when we encounter an empty cell (weight 0), we add it to the front of the queue, and when we encounter an obstacle (weight 1), we add it to the back of the queue. This ensures we always process paths with fewer obstacles first.
Intuition
The key insight is recognizing that this problem is about finding the "cheapest" path through a grid, where the cost is the number of obstacles we need to remove.
Think of it this way: imagine you're walking through a maze where some paths are clear (empty cells) and others have barriers you can break through (obstacles). Walking through a clear path is free, but breaking through a barrier costs you energy. You want to reach the exit using the least amount of energy.
This naturally translates to a shortest path problem with edge weights:
- Moving to an empty cell (0) → cost = 0
- Moving to an obstacle (1) → cost = 1
Now, why use BFS with a deque instead of regular BFS or Dijkstra's?
Regular BFS assumes all edges have the same weight, which isn't true here. Dijkstra's algorithm works but is overkill since we only have two possible weights (0 and 1).
The clever trick is using a double-ended queue (deque) to maintain the BFS invariant that nodes are processed in order of their distance from the source. Here's the intuition:
- When we move to an empty cell (cost 0), the total cost stays the same, so we add it to the front of the queue
- When we move to an obstacle (cost 1), the total cost increases by 1, so we add it to the back of the queue
This ensures that at any point, all nodes in the queue with cost k
are processed before any node with cost k+1
. It's like having two priority levels: "free moves" get processed immediately, while "costly moves" wait their turn.
This 0-1 BFS technique guarantees we find the minimum cost path because we always explore all paths with fewer obstacles before considering paths with more obstacles.
Learn more about Breadth-First Search, Graph, Shortest Path and Heap (Priority Queue) patterns.
Solution Approach
The solution implements a 0-1 BFS using a double-ended queue, which is optimal for graphs with edge weights of only 0 and 1.
Data Structures Used:
deque
: A double-ended queue to implement 0-1 BFSvis
(set): To track visited cells and avoid revisiting them- Each queue element: tuple
(i, j, k)
where(i, j)
is the position andk
is the number of obstacles removed so far
Algorithm Steps:
-
Initialization: Start with the source cell
(0, 0)
with 0 obstacles removed. Add(0, 0, 0)
to the deque. -
Direction vectors: Use
dirs = (-1, 0, 1, 0, -1)
withpairwise
to generate the four directions. Each pair(dirs[i], dirs[i+1])
gives us:(-1, 0)
: up(0, 1)
: right(1, 0)
: down(0, -1)
: left
-
BFS Loop: Continue until we reach the destination:
- Pop from the front of the deque (always the minimum cost node)
- If we've reached
(m-1, n-1)
, return the costk
- If already visited, skip this cell
- Mark the cell as visited
-
Exploring Neighbors: For each valid neighbor
(x, y)
:- Check boundaries:
0 <= x < m
and0 <= y < n
- If
grid[x][y] == 0
(empty cell):- Add
(x, y, k)
to the front of deque (same cost)
- Add
- If
grid[x][y] == 1
(obstacle):- Add
(x, y, k+1)
to the back of deque (cost increases by 1)
- Add
- Check boundaries:
Why This Works:
The 0-1 BFS maintains the invariant that all nodes with distance d
are processed before any node with distance d+1
. By using appendleft()
for 0-weight edges and append()
for 1-weight edges, we ensure that:
- Cells reachable with
k
obstacles are always explored before cells requiringk+1
obstacles - The first time we reach the destination, we've found the minimum cost path
Time Complexity: O(m × n)
- Each cell is visited at most once
Space Complexity: O(m × n)
- For the visited set and queue in the worst case
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 this 3×3 grid:
[0, 1, 1] [1, 1, 0] [1, 0, 0]
We need to find the minimum obstacles to remove to get from (0,0) to (2,2).
Step-by-step execution:
Initial State:
- Deque:
[(0, 0, 0)]
- start at top-left with 0 obstacles removed - Visited: empty set
Iteration 1: Pop (0, 0, 0)
from front
- Current position: (0, 0), obstacles removed: 0
- Mark (0, 0) as visited
- Check neighbors:
- Right (0, 1): grid[0][1] = 1 (obstacle) → Add
(0, 1, 1)
to back of deque - Down (1, 0): grid[1][0] = 1 (obstacle) → Add
(1, 0, 1)
to back of deque
- Right (0, 1): grid[0][1] = 1 (obstacle) → Add
- Deque:
[(0, 1, 1), (1, 0, 1)]
Iteration 2: Pop (0, 1, 1)
from front
- Current position: (0, 1), obstacles removed: 1
- Mark (0, 1) as visited
- Check neighbors:
- Right (0, 2): grid[0][2] = 1 (obstacle) → Add
(0, 2, 2)
to back of deque - Down (1, 1): grid[1][1] = 1 (obstacle) → Add
(1, 1, 2)
to back of deque
- Right (0, 2): grid[0][2] = 1 (obstacle) → Add
- Deque:
[(1, 0, 1), (0, 2, 2), (1, 1, 2)]
Iteration 3: Pop (1, 0, 1)
from front
- Current position: (1, 0), obstacles removed: 1
- Mark (1, 0) as visited
- Check neighbors:
- Right (1, 1): grid[1][1] = 1 (obstacle) → Add
(1, 1, 2)
to back (already in queue) - Down (2, 0): grid[2][0] = 1 (obstacle) → Add
(2, 0, 2)
to back of deque
- Right (1, 1): grid[1][1] = 1 (obstacle) → Add
- Deque:
[(0, 2, 2), (1, 1, 2), (2, 0, 2)]
Iteration 4: Pop (0, 2, 2)
from front
- Current position: (0, 2), obstacles removed: 2
- Mark (0, 2) as visited
- Check neighbors:
- Down (1, 2): grid[1][2] = 0 (empty) → Add
(1, 2, 2)
to front of deque (same cost!)
- Down (1, 2): grid[1][2] = 0 (empty) → Add
- Deque:
[(1, 2, 2), (1, 1, 2), (2, 0, 2)]
Iteration 5: Pop (1, 2, 2)
from front
- Current position: (1, 2), obstacles removed: 2
- Mark (1, 2) as visited
- Check neighbors:
- Down (2, 2): grid[2][2] = 0 (empty) → Add
(2, 2, 2)
to front of deque
- Down (2, 2): grid[2][2] = 0 (empty) → Add
- Deque:
[(2, 2, 2), (1, 1, 2), (2, 0, 2)]
Iteration 6: Pop (2, 2, 2)
from front
- Current position: (2, 2) - We've reached the destination!
- Return the cost: 2 obstacles removed
Key Observations:
- When we moved from (0, 2) to (1, 2), both cells had the same total cost (2), but since (1, 2) was empty, we added it to the front of the deque, allowing it to be processed immediately.
- This ensured we explored the path through empty cells before considering paths that would require removing more obstacles.
- The path taken was: (0,0) → (0,1) → (0,2) → (1,2) → (2,2), removing 2 obstacles at positions (0,1) and (0,2).
Solution Implementation
1class Solution:
2 def minimumObstacles(self, grid: List[List[int]]) -> int:
3 # Get grid dimensions
4 rows, cols = len(grid), len(grid[0])
5
6 # Initialize deque with starting position (row, col, obstacles_removed)
7 queue = deque([(0, 0, 0)])
8
9 # Set to track visited cells
10 visited = set()
11
12 # Direction vectors for moving up, right, down, left
13 directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
14
15 # BFS with 0-1 optimization
16 while queue:
17 current_row, current_col, obstacles_removed = queue.popleft()
18
19 # Check if we reached the destination
20 if current_row == rows - 1 and current_col == cols - 1:
21 return obstacles_removed
22
23 # Skip if already visited
24 if (current_row, current_col) in visited:
25 continue
26
27 # Mark current cell as visited
28 visited.add((current_row, current_col))
29
30 # Explore all four directions
31 for delta_row, delta_col in directions:
32 next_row = current_row + delta_row
33 next_col = current_col + delta_col
34
35 # Check if next position is within grid bounds
36 if 0 <= next_row < rows and 0 <= next_col < cols:
37 if grid[next_row][next_col] == 0:
38 # Empty cell: add to front (0 cost)
39 queue.appendleft((next_row, next_col, obstacles_removed))
40 else:
41 # Obstacle: add to back (cost + 1)
42 queue.append((next_row, next_col, obstacles_removed + 1))
43
1class Solution {
2 public int minimumObstacles(int[][] grid) {
3 int rows = grid.length;
4 int cols = grid[0].length;
5
6 // Using deque for 0-1 BFS algorithm
7 // Each element: [row, col, obstacles_removed]
8 Deque<int[]> deque = new ArrayDeque<>();
9 deque.offer(new int[] {0, 0, 0});
10
11 // Direction vectors for moving up, right, down, left
12 int[] directions = {-1, 0, 1, 0, -1};
13
14 // Track visited cells to avoid revisiting
15 boolean[][] visited = new boolean[rows][cols];
16
17 // BFS traversal
18 while (true) {
19 // Get the next cell to process
20 int[] current = deque.poll();
21 int currentRow = current[0];
22 int currentCol = current[1];
23 int obstaclesRemoved = current[2];
24
25 // Check if we've reached the destination
26 if (currentRow == rows - 1 && currentCol == cols - 1) {
27 return obstaclesRemoved;
28 }
29
30 // Skip if already visited
31 if (visited[currentRow][currentCol]) {
32 continue;
33 }
34
35 // Mark current cell as visited
36 visited[currentRow][currentCol] = true;
37
38 // Explore all 4 adjacent cells
39 for (int dir = 0; dir < 4; dir++) {
40 int nextRow = currentRow + directions[dir];
41 int nextCol = currentCol + directions[dir + 1];
42
43 // Check if next cell is within bounds
44 if (nextRow >= 0 && nextRow < rows && nextCol >= 0 && nextCol < cols) {
45 if (grid[nextRow][nextCol] == 0) {
46 // Empty cell: add to front (higher priority, no cost increase)
47 deque.offerFirst(new int[] {nextRow, nextCol, obstaclesRemoved});
48 } else {
49 // Obstacle: add to back (lower priority, cost increases by 1)
50 deque.offerLast(new int[] {nextRow, nextCol, obstaclesRemoved + 1});
51 }
52 }
53 }
54 }
55 }
56}
57
1class Solution {
2public:
3 int minimumObstacles(vector<vector<int>>& grid) {
4 int rows = grid.size();
5 int cols = grid[0].size();
6
7 // Use deque for 0-1 BFS
8 // Each element stores: (row, col, obstacles_removed)
9 deque<tuple<int, int, int>> bfsQueue;
10 bfsQueue.push_back({0, 0, 0});
11
12 // Track visited cells to avoid revisiting
13 vector<vector<bool>> visited(rows, vector<bool>(cols, false));
14
15 // Direction vectors for moving up, right, down, left
16 int directions[5] = {-1, 0, 1, 0, -1};
17
18 // BFS traversal
19 while (!bfsQueue.empty()) {
20 // Get current position and obstacle count
21 auto [currentRow, currentCol, obstacleCount] = bfsQueue.front();
22 bfsQueue.pop_front();
23
24 // Check if we reached the destination
25 if (currentRow == rows - 1 && currentCol == cols - 1) {
26 return obstacleCount;
27 }
28
29 // Skip if already visited
30 if (visited[currentRow][currentCol]) {
31 continue;
32 }
33
34 // Mark current cell as visited
35 visited[currentRow][currentCol] = true;
36
37 // Explore all 4 adjacent cells
38 for (int dir = 0; dir < 4; ++dir) {
39 int nextRow = currentRow + directions[dir];
40 int nextCol = currentCol + directions[dir + 1];
41
42 // Check if next position is within bounds
43 if (nextRow >= 0 && nextRow < rows &&
44 nextCol >= 0 && nextCol < cols) {
45
46 if (grid[nextRow][nextCol] == 0) {
47 // Empty cell: add to front (same obstacle count)
48 bfsQueue.push_front({nextRow, nextCol, obstacleCount});
49 } else {
50 // Obstacle: add to back (increment obstacle count)
51 bfsQueue.push_back({nextRow, nextCol, obstacleCount + 1});
52 }
53 }
54 }
55 }
56
57 // This line should never be reached given valid input
58 return -1;
59 }
60};
61
1/**
2 * Finds the minimum number of obstacles to remove to reach from top-left to bottom-right
3 * Uses 0-1 BFS algorithm where moving to empty cells (0) has no cost and obstacles (1) have cost 1
4 * @param grid - 2D grid where 0 represents empty cell and 1 represents obstacle
5 * @returns Minimum number of obstacles that need to be removed
6 */
7function minimumObstacles(grid: number[][]): number {
8 const rows: number = grid.length;
9 const cols: number = grid[0].length;
10
11 // Four directions: right, left, down, up
12 const directions: number[][] = [
13 [0, 1], // right
14 [0, -1], // left
15 [1, 0], // down
16 [-1, 0], // up
17 ];
18
19 // Initialize distance matrix with infinity, representing minimum obstacles to reach each cell
20 const minObstacles: number[][] = Array.from(
21 { length: rows },
22 () => new Array(cols).fill(Infinity)
23 );
24
25 // Starting position has 0 obstacles removed
26 minObstacles[0][0] = 0;
27
28 // Use deque for 0-1 BFS (add to front for 0-cost edges, back for 1-cost edges)
29 const deque: number[][] = [[0, 0]];
30
31 while (deque.length > 0) {
32 // Process current cell from front of deque
33 const [currentRow, currentCol]: number[] = deque.shift()!;
34
35 // Explore all four adjacent cells
36 for (const [deltaRow, deltaCol] of directions) {
37 const nextRow: number = currentRow + deltaRow;
38 const nextCol: number = currentCol + deltaCol;
39
40 // Check if next position is within grid boundaries
41 if (nextRow < 0 || nextRow >= rows || nextCol < 0 || nextCol >= cols) {
42 continue;
43 }
44
45 // Cost to move to next cell (0 for empty, 1 for obstacle)
46 const moveCost: number = grid[nextRow][nextCol];
47
48 // Calculate total cost to reach next cell through current path
49 const newCost: number = minObstacles[currentRow][currentCol] + moveCost;
50
51 // Skip if current path is not better than existing path
52 if (newCost >= minObstacles[nextRow][nextCol]) {
53 continue;
54 }
55
56 // Update minimum obstacles for next cell
57 minObstacles[nextRow][nextCol] = newCost;
58
59 // Add next cell to deque for further exploration
60 // Note: For proper 0-1 BFS, should use unshift() for 0-cost edges
61 deque.push([nextRow, nextCol]);
62 }
63 }
64
65 // Return minimum obstacles to reach bottom-right corner
66 return minObstacles[rows - 1][cols - 1];
67}
68
Time and Space Complexity
Time Complexity: O(m * n)
where m
is the number of rows and n
is the number of columns in the grid.
The algorithm uses a modified BFS with a deque (0-1 BFS). Each cell in the grid is visited at most once due to the visited set vis
. For each cell, we explore at most 4 neighboring cells (constant time). Therefore, the total number of operations is proportional to the number of cells in the grid, which is m * n
.
Space Complexity: O(m * n)
The space complexity is dominated by:
- The visited set
vis
which can store up tom * n
cell coordinates in the worst case:O(m * n)
- The deque
q
which in the worst case could contain all cells before processing:O(m * n)
- The
dirs
tuple which takes constant space:O(1)
Overall space complexity is O(m * n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Using Standard BFS Instead of 0-1 BFS
Pitfall: A common mistake is using a standard BFS with a regular queue, treating all moves equally. This leads to incorrect results because standard BFS doesn't account for different edge weights (0 for empty cells, 1 for obstacles).
Why it fails: Standard BFS explores all cells at distance d
before moving to distance d+1
, but here "distance" means number of steps, not the cost. A path with more steps but fewer obstacles might be better than a shorter path with more obstacles.
Solution: Use a deque with 0-1 BFS pattern:
- Add 0-cost moves to the front (
appendleft
) - Add 1-cost moves to the back (
append
) - Always pop from the front (
popleft
)
2. Revisiting Cells with Different Costs
Pitfall: Not properly tracking visited cells can lead to:
- Infinite loops if you allow revisiting
- Suboptimal solutions if you mark cells as visited too early
Example scenario: You might reach a cell with cost 3, mark it visited, then later find a path to the same cell with cost 2, but skip it because it's already visited.
Solution: Check if a cell is visited AFTER popping it from the queue, not before adding it. This ensures that if we reach the same cell multiple times with different costs, we process the one with minimum cost first:
while queue: row, col, cost = queue.popleft() # Check visited AFTER popping, not before adding if (row, col) in visited: continue visited.add((row, col))
3. Incorrect Handling of Starting/Ending Cells
Pitfall: Assuming the starting cell (0,0)
or ending cell (m-1,n-1)
are always empty (value 0). If either contains an obstacle, you need to account for it.
Solution: The algorithm naturally handles this - if the starting cell has an obstacle, the first move will effectively "remove" it. If the ending cell has an obstacle, it will be counted when we reach it.
4. Using Dijkstra's Algorithm with a Priority Queue
Pitfall: While Dijkstra's algorithm would work correctly, it's overkill for this problem and less efficient.
Why it's suboptimal:
- Dijkstra's uses a priority queue with O(log n) operations
- 0-1 BFS uses a deque with O(1) operations
- Since we only have weights 0 and 1, 0-1 BFS is more efficient
Solution: Stick with 0-1 BFS for optimal performance when dealing with 0-1 weighted graphs.
Which of the following shows the order of node visit in a Breadth-first Search?
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
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Shortest Path Between A and B Prereq BFS on Graph problems graph_bfs Given an unweighted connected graph return the length of the shortest path between two nodes A and B in terms of the number of edges Assume there always exists a path between nodes A and B Input graph
Want a Structured Path to Master System Design Too? Don’t Miss This!