Shortest path with walls you may break
A grid holds 0 for an open cell and 1 for a wall. You start at the top-left
cell and want to reach the bottom-right cell, moving up, down, left or right one cell per step. You may walk through
at most k walls in total. Return the fewest steps needed, or −1 if it cannot be done. Both
corners are open.
Examples
| Input | Output | Why |
|---|---|---|
grid = [[0,0,0],[1,1,0],[0,0,0],[0,1,1],[0,0,0]], k = 1 | 6 | Break the wall at (3, 2): the path drops straight down the right side. |
grid = [[0,1,1],[1,1,1],[1,0,0]], k = 1 | -1 | Every route crosses at least two walls. |
grid = [[0,1,0,0,0],[0,1,0,1,0],[0,0,0,1,0]], k = 0 | 10 | No walls may be broken: the path winds around both. |
Constraints
- 1 ≤ rows, cols ≤ 40
- 0 ≤ k ≤ rows · cols
- grid[0][0] = grid[rows − 1][cols − 1] = 0
Hints
Hint 1
Plain breadth-first search over cells fails: reaching a cell with more walls still allowed is better, even if it took longer.
Hint 2
Make the state (row, col, walls left). Two visits to the same cell with different walls left are different states.
Hint 3
Breadth-first search over those states; the first time any state at the target is reached, its distance is the answer.
Where people lose marks
- Marking cells, not states, as visited: an early arrival with no walls left blocks a later, more useful one.
- Spending a wall on the target or the start, which are open.
- Forgetting the shortcut when k is large, so the state space becomes huge for no reason.
Dry run on [[0,0,0],[1,1,0],[0,0,0],[0,1,1],[0,0,0]],1
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 7
Start at (0, 0) with 1 wall left. Each reached cell shows the most walls still allowed on arrival.
-
Step 2 / 7
Step 1: 2 new states. Some moved through a wall and have one fewer left.
-
Step 3 / 7
Step 2: 4 new states. Some moved through a wall and have one fewer left.
-
Step 4 / 7
Step 3: 5 new states.
-
Step 5 / 7
Step 4: 4 new states.
-
Step 6 / 7
Step 5: 3 new states. Some moved through a wall and have one fewer left.
-
Step 7 / 7
Step 6: 3 new states. Some moved through a wall and have one fewer left. The corner is reached after 6 steps: that is the answer.
How to think about it
Every step costs one, so this is a breadth-first search, but over the right nodes. The rules depend on how many walls you can still break, so the state is (row, column, walls left). From a state, moving into an open cell keeps the walls left; moving into a wall uses one, and is allowed only if one remains. There are rows · cols · (k + 1) states, each visited at most once.
Two refinements help. If k ≥ rows + cols − 3, the straight Manhattan path is always possible, so the answer is rows + cols − 2 without searching. And a cell already reached with at least as many walls left need not be reached again with fewer.
Complexity. Time O(rows · cols · k) · Space O(rows · cols · k)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
from collections import deque
def shortest_path_breaking(grid, k):
rows, cols = len(grid), len(grid[0])
if k >= rows + cols - 3:
return rows + cols - 2 # enough to walk straight through anything
seen = {(0, 0, k)}
q = deque([(0, 0, k, 0)]) # (row, col, walls left, steps)
while q:
r, c, left, steps = q.popleft()
if r == rows - 1 and c == cols - 1:
return steps
for a, b in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= a < rows and 0 <= b < cols:
nl = left - grid[a][b] # a wall costs one
if nl >= 0 and (a, b, nl) not in seen:
seen.add((a, b, nl))
q.append((a, b, nl, steps + 1))
return -1#include <array>
#include <queue>
#include <vector>
int solve(const std::vector<std::vector<int>>& grid, int k) {
const int rows = grid.size(), cols = grid[0].size();
if (k >= rows + cols - 3) return rows + cols - 2; // straight through anything
std::vector<std::vector<std::vector<bool>>> seen(rows, std::vector<std::vector<bool>>(cols, std::vector<bool>(k + 1, false)));
std::queue<std::array<int, 4>> q; // row, col, walls left, steps
q.push({0, 0, k, 0});
seen[0][0][k] = true;
const int dr[] = {1, -1, 0, 0}, dc[] = {0, 0, 1, -1};
while (!q.empty()) {
auto [r, c, left, steps] = q.front();
q.pop();
if (r == rows - 1 && c == cols - 1) return steps;
for (int d = 0; d < 4; ++d) {
int a = r + dr[d], b = c + dc[d];
if (a < 0 || b < 0 || a >= rows || b >= cols) continue;
int nl = left - grid[a][b]; // a wall costs one
if (nl >= 0 && !seen[a][b][nl]) { seen[a][b][nl] = true; q.push({a, b, nl, steps + 1}); }
}
}
return -1;
}import java.util.*;
class Solution {
public int shortestPathBreaking(int[][] grid, int k) {
int rows = grid.length, cols = grid[0].length;
if (k >= rows + cols - 3) return rows + cols - 2; // straight through anything
boolean[][][] seen = new boolean[rows][cols][k + 1];
ArrayDeque<int[]> q = new ArrayDeque<>(); // row, col, walls left, steps
q.add(new int[]{0, 0, k, 0});
seen[0][0][k] = true;
int[] dr = {1, -1, 0, 0}, dc = {0, 0, 1, -1};
while (!q.isEmpty()) {
int[] s = q.poll();
if (s[0] == rows - 1 && s[1] == cols - 1) return s[3];
for (int d = 0; d < 4; d++) {
int a = s[0] + dr[d], b = s[1] + dc[d];
if (a < 0 || b < 0 || a >= rows || b >= cols) continue;
int nl = s[2] - grid[a][b]; // a wall costs one
if (nl >= 0 && !seen[a][b][nl]) { seen[a][b][nl] = true; q.add(new int[]{a, b, nl, s[3] + 1}); }
}
}
return -1;
}
}function shortestPathBreaking(grid, k) {
const rows = grid.length, cols = grid[0].length;
if (k >= rows + cols - 3) return rows + cols - 2; // straight through anything
const seen = new Uint8Array(rows * cols * (k + 1)); // state (r, c, walls left)
const id = (r, c, w) => (r * cols + c) * (k + 1) + w;
seen[id(0, 0, k)] = 1;
let ring = [[0, 0, k]];
for (let steps = 0; ring.length; steps++) {
const next = [];
for (const [r, c, left] of ring) {
if (r === rows - 1 && c === cols - 1) return steps;
for (const [a, b] of [[r + 1, c], [r - 1, c], [r, c + 1], [r, c - 1]]) {
if (a < 0 || b < 0 || a >= rows || b >= cols) continue;
const nl = left - grid[a][b]; // a wall costs one
if (nl >= 0 && !seen[id(a, b, nl)]) { seen[id(a, b, nl)] = 1; next.push([a, b, nl]); }
}
}
ring = next;
}
return -1;
}use std::collections::VecDeque;
fn solve<const C: usize>(grid: &[[i64; C]], k: i64) -> i64 {
let rows = grid.len();
let k = k as usize;
if k + 3 >= rows + C {
return (rows + C - 2) as i64; // straight through anything
}
let mut seen = vec![vec![vec![false; k + 1]; C]; rows];
let mut q = VecDeque::from([(0usize, 0usize, k, 0i64)]); // row, col, walls left, steps
seen[0][0][k] = true;
while let Some((r, c, left, steps)) = q.pop_front() {
if r == rows - 1 && c == C - 1 {
return steps;
}
let around = [(r + 1, c), (r.wrapping_sub(1), c), (r, c + 1), (r, c.wrapping_sub(1))];
for (a, b) in around {
if a >= rows || b >= C {
continue;
}
let wall = grid[a][b] as usize;
if left < wall {
continue; // no wall left to break
}
let nl = left - wall;
if !seen[a][b][nl] {
seen[a][b][nl] = true;
q.push_back((a, b, nl, steps + 1));
}
}
}
-1
}import qualified Data.Set as S
import Data.Sequence (Seq (..), (|>))
import qualified Data.Sequence as Q
solve :: [[Int]] -> Int -> Int
solve grid k
| k >= rows + cols - 3 = rows + cols - 2 -- straight through anything
| otherwise = bfs (Q.singleton ((0, 0, k), 0)) (S.singleton (0, 0, k))
where
rows = length grid
cols = length (head grid)
cell r c = (grid !! r) !! c
-- states are (row, col, walls left)
bfs Empty _ = -1
bfs (((r, c, left), steps) :<| q) seen
| r == rows - 1 && c == cols - 1 = steps
| otherwise =
let next = [ (a, b, left - cell a b)
| (a, b) <- [(r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)]
, a >= 0, b >= 0, a < rows, b < cols
, left - cell a b >= 0
, not (S.member (a, b, left - cell a b) seen) ]
in bfs (foldl (|>) q [(s, steps + 1) | s <- next]) (foldr S.insert seen next)
Variants to try
- Return the walls to break, not just the length.
- Breaking a wall costs time instead of being limited in number: Dijkstra, or 0-1 BFS.
- Keys open doors of matching colours: the state becomes (cell, set of keys held).
Next problems
Shortest path through a grid
Breadth-first search reaches every cell in the fewest moves because it explores in rings of equal distance. Why it beats depth-first search is the lesson.
Open the problem → Medium20 minDistance to the nearest zero
For every cell of a 0/1 grid, return the number of steps to the nearest 0. One breadth-first search that starts from all the zeros at once.
Open the problem → Medium18 minMinutes until every orange rots
Start the search from every rotten orange at once. The answer is the number of rings it takes to reach the last fresh one.
Open the problem →Write a solution and run it against the real test table.
Keep going
Shortest path through a grid
Breadth-first search reaches every cell in the fewest moves because it explores in rings of equal distance. Why it beats depth-first search is the lesson.
Open the problem → Medium20 minDistance to the nearest zero
For every cell of a 0/1 grid, return the number of steps to the nearest 0. One breadth-first search that starts from all the zeros at once.
Open the problem → Medium18 minMinutes until every orange rots
Start the search from every rotten orange at once. The answer is the number of rings it takes to reach the last fresh one.
Open the problem → GraphsThe pattern behind it
Grids, networks and dependencies are all graphs. Breadth-first search finds shortest paths, depth-first search and union-find find what is connected, Dijkstra handles weighted edges, and a queue of nodes with no remaining prerequisites gives an order.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.