Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Level-by-level exploration: BFS explores cells in waves, allowing us to systematically check all possible paths
  2. Optimal substructure: The minimum cost to reach a cell depends on the minimum costs to reach its neighbors
  3. 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 where dist[i][j] stores the minimum health points lost to reach cell (i, j)
  • q: A deque (double-ended queue) for BFS traversal
  • dirs: A tuple storing direction vectors (-1, 0, 1, 0, -1) for efficient iteration

Algorithm Steps:

  1. 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
  2. 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)
      • 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
  3. Check the result:

    • After BFS completes, dist[m-1][n-1] contains the minimum health lost
    • Return True if dist[m-1][n-1] < health, meaning we can reach the destination with health remaining

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 Evaluator

Example 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, update dist[0][1] = 1 and add (0, 1) to queue
    • Down (1, 0): valid
      • New cost = dist[0][0] + grid[1][0] = 0 + 1 = 1
      • Current dist[1][0] = inf
      • Since 1 < inf, update dist[1][0] = 1 and add (1, 0) to queue
    dist = [[0, 1],
            [1, inf]]
    q = [(0, 1), (1, 0)]

Step 3: Process (0, 1)

  • Dequeue (0, 1)

  • Check adjacent cells:

    • Left (0, 0): cost would be 1 + 0 = 1, but dist[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, update dist[1][1] = 1 and add (1, 1) to queue
    dist = [[0, 1],
            [1, 1]]
    q = [(1, 0), (1, 1)]

Step 4: Process (1, 0)

  • Dequeue (1, 0)
  • Check adjacent cells:
    • Up (0, 0): cost would be 1 + 0 = 1, but dist[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

Step 5: Process (1, 1)

  • Dequeue (1, 1)
  • Check adjacent cells:
    • Up (0, 1): cost would be 1 + 1 = 2, but dist[0][1] = 1 is already better
    • Left (1, 0): cost would be 1 + 1 = 2, but dist[1][0] = 1 is already better

Final Result:

  • dist[1][1] = 1 (minimum health lost to reach destination)
  • Since 1 < 3 (health), return True

The algorithm found two equally good paths:

  1. (0,0) → (0,1) → (1,1) with health loss: 0 + 1 + 0 = 1
  2. (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 is O(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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More