3286. Find a Safe Walk Through a Grid
Problem Description
You have an m x n
binary grid and a starting health value. Your goal is to navigate from the top-left corner (0, 0)
to the bottom-right corner (m-1, n-1)
while keeping your health positive.
Movement Rules:
- You can move in four directions: up, down, left, or right to adjacent cells
- You can only move if your health remains positive after the move
Health Mechanics:
- You start with an initial
health
value - Cells with
grid[i][j] = 1
are unsafe and reduce your health by 1 when you enter them - Cells with
grid[i][j] = 0
are safe and don't affect your health
Objective:
Determine if it's possible to reach the destination cell (m-1, n-1)
with at least 1 health point remaining. Return true
if possible, false
otherwise.
Example: If you have a grid where some cells are unsafe (value 1), you need to find if there exists a path from start to finish that doesn't reduce your health below 1. The path doesn't have to be the shortest - it just needs to preserve enough health to reach the destination alive.
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 adjacent cells are connected by edges. We need to find a path from the start node (0,0) to the end node (m-1, n-1).
Is it a tree?
- No: The grid structure is not a tree. There can be multiple paths between cells, and cycles are possible when moving in four directions.
Is the problem related to directed acyclic graphs?
- No: The grid allows movement in all four directions, which means it can have cycles. It's not a DAG.
Is the problem related to shortest paths?
- Yes: We need to find the path with minimum health reduction (or minimum cost) from start to finish. This is essentially finding the shortest/optimal path in terms of health cost.
Is the graph Weighted?
- No: While cells have different costs (0 or 1), this is more about uniform edge weights where each move has the same weight, but nodes have different costs. The problem can be solved with BFS by tracking the minimum cost to reach each cell.
Conclusion: BFS The flowchart leads us to BFS (Breadth-First Search). This makes sense because:
- We're exploring a grid graph level by level
- We need to find the minimum health cost path
- BFS can efficiently track the minimum cost to reach each cell
- The solution uses a queue-based approach with distance tracking, which is characteristic of BFS
The algorithm maintains a distance array to track the minimum health reduction needed to reach each cell, updating it as we explore the grid using BFS traversal.
Intuition
When we think about this problem, we need to find if there's a path from start to finish that doesn't drain our health completely. The key insight is that we want to minimize the total health lost along any path.
Instead of tracking our remaining health as we explore (which would be complicated since we could reach the same cell with different health values), we can flip the problem: track the minimum health cost required to reach each cell.
This transforms the problem into finding the path with minimum cost. If we know the minimum health needed to reach any cell, we can simply check if the minimum health needed to reach the destination is less than our starting health.
Why BFS works here:
- Level-by-level exploration: BFS explores cells in waves, allowing us to systematically check all possible paths
- Optimal substructure: The minimum cost to reach a cell depends on the minimum costs to reach its neighbors
- Update when better: As we explore, we only update a cell's cost when we find a cheaper way to reach it
The clever part is using a dist
array where dist[i][j]
represents the minimum health points we'd lose to reach cell (i, j)
. We start with dist[0][0] = grid[0][0]
(the cost of the starting cell itself).
As we process each cell from the queue, we check all four adjacent cells. If moving to an adjacent cell gives us a lower total cost than what we previously recorded, we update it and add that cell back to the queue for further exploration.
This approach ensures we always find the path with minimum health loss. At the end, if dist[m-1][n-1] < health
, we know we can reach the destination with health remaining.
Learn more about Breadth-First Search, Graph, Shortest Path and Heap (Priority Queue) patterns.
Solution Approach
The solution implements BFS to find the minimum health cost path from the top-left to the bottom-right corner.
Data Structures Used:
dist
: A 2D array wheredist[i][j]
stores the minimum health points lost to reach cell(i, j)
q
: A deque (double-ended queue) for BFS traversaldirs
: A tuple storing direction vectors(-1, 0, 1, 0, -1)
for efficient iteration
Algorithm Steps:
-
Initialize the distance matrix:
- Create
dist
with all values set to infinity (inf
) - Set
dist[0][0] = grid[0][0]
(starting cell's cost) - Add starting position
(0, 0)
to the queue
- Create
-
BFS traversal:
- While the queue is not empty:
- Dequeue a cell
(x, y)
- Check all four adjacent cells using
pairwise(dirs)
- This cleverly generates pairs:
(-1, 0)
,(0, 1)
,(1, 0)
,(0, -1)
- This cleverly generates pairs:
- For each valid adjacent cell
(nx, ny)
:- Calculate new cost:
dist[x][y] + grid[nx][ny]
- If this cost is less than
dist[nx][ny]
:- Update
dist[nx][ny]
with the new minimum cost - Add
(nx, ny)
to the queue for further exploration
- Update
- Calculate new cost:
- Dequeue a cell
- While the queue is not empty:
-
Check the result:
- After BFS completes,
dist[m-1][n-1]
contains the minimum health lost - Return
True
ifdist[m-1][n-1] < health
, meaning we can reach the destination with health remaining
- After BFS completes,
Key Optimization: The algorithm only adds a cell back to the queue when we find a better (lower cost) path to it. This ensures we explore all possible paths efficiently while maintaining the minimum cost to each cell.
Time Complexity: O(m × n)
where each cell can be visited multiple times in worst case
Space Complexity: O(m × n)
for the distance matrix and queue
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Given:
- Grid:
[[0, 1], [1, 0]]
(2x2 grid) - Initial health:
3
- Start:
(0, 0)
, Destination:(1, 1)
Grid visualization: [0, 1] -> 0 = safe, 1 = unsafe (-1 health) [1, 0]
Step 1: Initialization
- Create
dist
matrix initialized with infinity:dist = [[inf, inf], [inf, inf]]
- Set
dist[0][0] = grid[0][0] = 0
(starting cell cost)dist = [[0, inf], [inf, inf]]
- Add
(0, 0)
to queue:q = [(0, 0)]
Step 2: Process (0, 0)
-
Dequeue
(0, 0)
-
Check adjacent cells:
- Right
(0, 1)
: valid- New cost =
dist[0][0] + grid[0][1] = 0 + 1 = 1
- Current
dist[0][1] = inf
- Since
1 < inf
, updatedist[0][1] = 1
and add(0, 1)
to queue
- New cost =
- Down
(1, 0)
: valid- New cost =
dist[0][0] + grid[1][0] = 0 + 1 = 1
- Current
dist[1][0] = inf
- Since
1 < inf
, updatedist[1][0] = 1
and add(1, 0)
to queue
- New cost =
dist = [[0, 1], [1, inf]] q = [(0, 1), (1, 0)]
- Right
Step 3: Process (0, 1)
-
Dequeue
(0, 1)
-
Check adjacent cells:
- Left
(0, 0)
: cost would be1 + 0 = 1
, butdist[0][0] = 0
is already better - Down
(1, 1)
: valid- New cost =
dist[0][1] + grid[1][1] = 1 + 0 = 1
- Current
dist[1][1] = inf
- Since
1 < inf
, updatedist[1][1] = 1
and add(1, 1)
to queue
- New cost =
dist = [[0, 1], [1, 1]] q = [(1, 0), (1, 1)]
- Left
Step 4: Process (1, 0)
- Dequeue
(1, 0)
- Check adjacent cells:
- Up
(0, 0)
: cost would be1 + 0 = 1
, butdist[0][0] = 0
is already better - Right
(1, 1)
:- New cost =
dist[1][0] + grid[1][1] = 1 + 0 = 1
- Current
dist[1][1] = 1
- Since
1 = 1
(not less), no update needed
- New cost =
- Up
Step 5: Process (1, 1)
- Dequeue
(1, 1)
- Check adjacent cells:
- Up
(0, 1)
: cost would be1 + 1 = 2
, butdist[0][1] = 1
is already better - Left
(1, 0)
: cost would be1 + 1 = 2
, butdist[1][0] = 1
is already better
- Up
Final Result:
dist[1][1] = 1
(minimum health lost to reach destination)- Since
1 < 3
(health), returnTrue
The algorithm found two equally good paths:
(0,0) → (0,1) → (1,1)
with health loss: 0 + 1 + 0 = 1(0,0) → (1,0) → (1,1)
with health loss: 0 + 1 + 0 = 1
Both paths lose only 1 health point, leaving us with 2 health remaining at the destination.
Solution Implementation
1from collections import deque
2from typing import List
3from math import inf
4from itertools import pairwise
5
6
7class Solution:
8 def findSafeWalk(self, grid: List[List[int]], health: int) -> bool:
9 # Get grid dimensions
10 rows, cols = len(grid), len(grid[0])
11
12 # Initialize distance matrix with infinity
13 # distance[i][j] represents minimum cost to reach cell (i, j)
14 distance = [[inf] * cols for _ in range(rows)]
15
16 # Starting cell cost is the value of grid[0][0]
17 distance[0][0] = grid[0][0]
18
19 # Initialize BFS queue with starting position
20 queue = deque([(0, 0)])
21
22 # Direction vectors for moving up, right, down, left
23 # Using pairwise will create: (-1, 0), (0, 1), (1, 0), (0, -1)
24 directions = (-1, 0, 1, 0, -1)
25
26 # BFS to find minimum cost path
27 while queue:
28 current_x, current_y = queue.popleft()
29
30 # Explore all 4 adjacent cells
31 for delta_x, delta_y in pairwise(directions):
32 next_x = current_x + delta_x
33 next_y = current_y + delta_y
34
35 # Check if next position is valid and if we found a better path
36 if (0 <= next_x < rows and
37 0 <= next_y < cols and
38 distance[next_x][next_y] > distance[current_x][current_y] + grid[next_x][next_y]):
39
40 # Update minimum cost to reach this cell
41 distance[next_x][next_y] = distance[current_x][current_y] + grid[next_x][next_y]
42
43 # Add this cell to queue for further exploration
44 queue.append((next_x, next_y))
45
46 # Check if we can reach bottom-right corner with health remaining
47 return distance[-1][-1] < health
48
1class Solution {
2 public boolean findSafeWalk(List<List<Integer>> grid, int health) {
3 // Get grid dimensions
4 int rows = grid.size();
5 int cols = grid.get(0).size();
6
7 // Initialize distance array to track minimum health cost to reach each cell
8 int[][] minCost = new int[rows][cols];
9 for (int[] row : minCost) {
10 Arrays.fill(row, Integer.MAX_VALUE);
11 }
12
13 // Set starting position cost (health lost at starting cell)
14 minCost[0][0] = grid.get(0).get(0);
15
16 // Initialize BFS queue with starting position
17 Deque<int[]> queue = new ArrayDeque<>();
18 queue.offer(new int[] {0, 0});
19
20 // Direction vectors for moving up, right, down, left
21 final int[] directions = {-1, 0, 1, 0, -1};
22
23 // BFS to find minimum cost path
24 while (!queue.isEmpty()) {
25 int[] currentPosition = queue.poll();
26 int currentRow = currentPosition[0];
27 int currentCol = currentPosition[1];
28
29 // Explore all 4 adjacent cells
30 for (int i = 0; i < 4; i++) {
31 int nextRow = currentRow + directions[i];
32 int nextCol = currentCol + directions[i + 1];
33
34 // Check if next position is valid and if we found a better path
35 if (nextRow >= 0 && nextRow < rows &&
36 nextCol >= 0 && nextCol < cols &&
37 minCost[nextRow][nextCol] > minCost[currentRow][currentCol] + grid.get(nextRow).get(nextCol)) {
38
39 // Update minimum cost for the next cell
40 minCost[nextRow][nextCol] = minCost[currentRow][currentCol] + grid.get(nextRow).get(nextCol);
41
42 // Add next position to queue for further exploration
43 queue.offer(new int[] {nextRow, nextCol});
44 }
45 }
46 }
47
48 // Check if we can reach destination with health remaining
49 return minCost[rows - 1][cols - 1] < health;
50 }
51}
52
1class Solution {
2public:
3 bool findSafeWalk(vector<vector<int>>& grid, int health) {
4 // Get grid dimensions
5 int rows = grid.size();
6 int cols = grid[0].size();
7
8 // Initialize distance matrix to track minimum cost to reach each cell
9 // dist[i][j] represents the minimum health lost to reach cell (i, j)
10 vector<vector<int>> dist(rows, vector<int>(cols, INT_MAX));
11
12 // Starting point cost is the value at grid[0][0]
13 dist[0][0] = grid[0][0];
14
15 // BFS queue to explore cells
16 queue<pair<int, int>> bfsQueue;
17 bfsQueue.emplace(0, 0);
18
19 // Direction vectors for moving up, right, down, left
20 // dirs[i] and dirs[i+1] form (dx, dy) pairs
21 int dirs[5] = {-1, 0, 1, 0, -1};
22
23 // BFS to find minimum cost path
24 while (!bfsQueue.empty()) {
25 // Get current cell coordinates
26 auto [currentX, currentY] = bfsQueue.front();
27 bfsQueue.pop();
28
29 // Explore all 4 adjacent cells
30 for (int i = 0; i < 4; ++i) {
31 int nextX = currentX + dirs[i];
32 int nextY = currentY + dirs[i + 1];
33
34 // Check if next cell is within bounds and if we found a better path
35 if (nextX >= 0 && nextX < rows &&
36 nextY >= 0 && nextY < cols &&
37 dist[nextX][nextY] > dist[currentX][currentY] + grid[nextX][nextY]) {
38
39 // Update minimum cost to reach the next cell
40 dist[nextX][nextY] = dist[currentX][currentY] + grid[nextX][nextY];
41
42 // Add the cell to queue for further exploration
43 bfsQueue.emplace(nextX, nextY);
44 }
45 }
46 }
47
48 // Check if we can reach destination with remaining health
49 return dist[rows - 1][cols - 1] < health;
50 }
51};
52
1/**
2 * Determines if there exists a safe walk path from top-left to bottom-right corner
3 * @param grid - 2D array where each cell contains a value (0 or 1) representing danger level
4 * @param health - Initial health points available for the journey
5 * @returns true if a path exists with total danger less than health, false otherwise
6 */
7function findSafeWalk(grid: number[][], health: number): boolean {
8 // Get grid dimensions
9 const rows: number = grid.length;
10 const cols: number = grid[0].length;
11
12 // Initialize distance matrix to track minimum danger to reach each cell
13 // All cells start with Infinity except the starting point
14 const minDanger: number[][] = Array.from(
15 { length: rows },
16 () => Array(cols).fill(Infinity)
17 );
18
19 // Set starting point danger value
20 minDanger[0][0] = grid[0][0];
21
22 // Initialize BFS queue with starting position
23 const queue: [number, number][] = [[0, 0]];
24
25 // Direction vectors for moving up, right, down, left
26 // Using a single array: [dx1, dy1, dx2, dy2, dx3, dy3, dx4, dy4, sentinel]
27 const directions: number[] = [-1, 0, 1, 0, -1];
28
29 // Process queue using BFS to find minimum danger paths
30 while (queue.length > 0) {
31 // Dequeue current position
32 const [currentRow, currentCol] = queue.shift()!;
33
34 // Explore all four adjacent cells
35 for (let i = 0; i < 4; i++) {
36 // Calculate next position using direction vectors
37 const nextRow: number = currentRow + directions[i];
38 const nextCol: number = currentCol + directions[i + 1];
39
40 // Check if next position is valid and if we found a better path
41 if (
42 nextRow >= 0 &&
43 nextRow < rows &&
44 nextCol >= 0 &&
45 nextCol < cols &&
46 minDanger[nextRow][nextCol] > minDanger[currentRow][currentCol] + grid[nextRow][nextCol]
47 ) {
48 // Update minimum danger for the next cell
49 minDanger[nextRow][nextCol] = minDanger[currentRow][currentCol] + grid[nextRow][nextCol];
50
51 // Add next position to queue for further exploration
52 queue.push([nextRow, nextCol]);
53 }
54 }
55 }
56
57 // Check if minimum danger to reach destination is less than available health
58 return minDanger[rows - 1][cols - 1] < health;
59}
60
Time and Space Complexity
Time Complexity: O(m × n × m × n)
where m
is the number of rows and n
is the number of columns in the grid.
The algorithm uses BFS with a deque. In the worst case, each cell (x, y)
can be visited multiple times - specifically, it can be added to the queue whenever a shorter distance is found from any of its neighbors. Since we don't use a visited set and rely only on distance comparison, a cell can potentially be processed up to O(m × n)
times (once for each possible path leading to it). With m × n
total cells, this gives us O(m × n × m × n)
time complexity.
Space Complexity: O(m × n)
where m
is the number of rows and n
is the number of columns in the grid.
The space is used for:
- The
dist
matrix:O(m × n)
- The deque
q
: In the worst case, it can contain all cells, which isO(m × n)
- Other variables use constant space
Therefore, the total space complexity is O(m × n)
.
Note: The reference answer suggests O(m × n)
time complexity, which would be achievable with Dijkstra's algorithm using a priority queue or with 0-1 BFS (using deque with appendleft for 0-weight edges). However, the given implementation doesn't optimize for this and may revisit cells multiple times, leading to a higher time complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Using Standard BFS Instead of 0-1 BFS
The current solution uses a standard BFS with a regular deque, which can lead to suboptimal performance and potentially incorrect results when cells are revisited multiple times.
The Problem:
- Standard BFS doesn't guarantee that when we first visit a cell, we've found the minimum cost path to it
- This causes cells to be added to the queue multiple times, leading to redundant processing
- In worst case, a cell could be processed O(m×n) times
The Solution - 0-1 BFS: Since the grid only contains 0s and 1s (weights of 0 or 1), we can optimize using 0-1 BFS:
from collections import deque
class Solution:
def findSafeWalk(self, grid: List[List[int]], health: int) -> bool:
rows, cols = len(grid), len(grid[0])
distance = [[inf] * cols for _ in range(rows)]
distance[0][0] = grid[0][0]
# Use deque for 0-1 BFS
queue = deque([(0, 0)])
directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
while queue:
x, y = queue.popleft()
# Skip if we've already found a better path
if distance[x][y] < distance[x][y]:
continue
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < rows and 0 <= ny < cols:
new_cost = distance[x][y] + grid[nx][ny]
if new_cost < distance[nx][ny]:
distance[nx][ny] = new_cost
# Key optimization: add to front if safe cell (0), back if unsafe (1)
if grid[nx][ny] == 0:
queue.appendleft((nx, ny))
else:
queue.append((nx, ny))
return distance[-1][-1] < health
2. Not Using Dijkstra's Algorithm for Guaranteed Optimality
For complete correctness and optimal performance, Dijkstra's algorithm with a priority queue is more appropriate:
import heapq
class Solution:
def findSafeWalk(self, grid: List[List[int]], health: int) -> bool:
rows, cols = len(grid), len(grid[0])
# Priority queue: (cost, x, y)
pq = [(grid[0][0], 0, 0)]
visited = set()
directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
while pq:
cost, x, y = heapq.heappop(pq)
# Skip if already visited
if (x, y) in visited:
continue
visited.add((x, y))
# Check if we reached destination
if x == rows - 1 and y == cols - 1:
return cost < health
for dx, dy in directions:
nx, ny = x + dx, y + dy
if (0 <= nx < rows and 0 <= ny < cols and
(nx, ny) not in visited):
heapq.heappush(pq, (cost + grid[nx][ny], nx, ny))
return False
This approach guarantees that when we visit a cell for the first time, we've found the minimum cost path to it, eliminating redundant processing.
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
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!