286. Walls And Gates - Explanation

Problem Link

Description

You are given a m×nm \times n 2D grid initialized with these three possible values:

  1. -1 - A water cell that can not be traversed.
  2. 0 - A treasure chest.
  3. INF - A land cell that can be traversed. We use the integer 2^31 - 1 = 2147483647 to represent INF.

Fill each land cell with the distance to its nearest treasure chest. If a land cell cannot reach a treasure chest then the value should remain INF.

Assume the grid can only be traversed up, down, left, or right.

Modify the grid in-place.

Example 1:

Input: [
  [2147483647,-1,0,2147483647],
  [2147483647,2147483647,2147483647,-1],
  [2147483647,-1,2147483647,-1],
  [0,-1,2147483647,2147483647]
]

Output: [
  [3,-1,0,1],
  [2,2,1,-1],
  [1,-1,2,-1],
  [0,-1,3,4]
]

Example 2:

Input: [
  [0,-1],
  [2147483647,2147483647]
]

Output: [
  [0,-1],
  [1,2]
]

Constraints:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 100
  • grid[i][j] is one of {-1, 0, 2147483647}


Topics

Recommended Time & Space Complexity

You should aim for a solution with O(m * n) time and O(m * n) space, where m is the number of rows and n is the number of columns in the given grid.


Hint 1

A brute force solution would be to iterate on each land cell and run a BFS from that cells to find the nearest treasure chest. This would be an O((m * n)^2) solution. Can you think of a better way? Sometimes it is not optimal to go from source to destination.


Hint 2

We can see that instead of going from every cell to find the nearest treasure chest, we can do it in reverse. We can just do a BFS from all the treasure chests in grid and just explore all possible paths from those chests. Why? Because in this approach, the treasure chests self mark the cells level by level and the level number will be the distance from that cell to a treasure chest. We don't revisit a cell. This approach is called Multi-Source BFS. How would you implement it?


Hint 3

We insert all the cells (row, col) that represent the treasure chests into the queue. Then, we process the cells level by level, handling all the current cells in the queue at once. For each cell, we mark it as visited and store the current level value as the distance at that cell. We then try to add the neighboring cells (adjacent cells) to the queue, but only if they have not been visited and are land cells.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • 2D Array (Grid) Traversal - Iterating through rows and columns of a matrix
  • Breadth First Search (BFS) - Finding shortest paths in unweighted graphs/grids
  • Multi-Source BFS - Starting BFS from multiple sources simultaneously for optimal distance calculation
  • Backtracking - Exploring all paths with state restoration for brute force solutions

1. Brute Force (Backtracking)

Intuition

For every empty cell (INF), we try to find the shortest path to any treasure (0) by exploring all 4 directions using backtracking DFS.

  • DFS explores all possible paths from the cell until it hits a treasure (distance 0), a wall (-1), or goes out of bounds.
  • We use a visited grid so the current path doesn't revisit cells (prevents infinite loops in cycles).
  • The answer for that cell is the minimum distance found among all DFS paths.

This works but is slow because we repeat DFS from many cells and re-explore the same areas again and again.

Algorithm

  1. Let INF represent empty rooms that need a distance.
  2. For each cell in the grid:
    • If it is INF, run DFS(cell) to compute the minimum distance to a 0.
  3. DFS(r, c):
    • If out of bounds / wall (-1) / already visited → return INF.
    • If current cell is a treasure (0) → return 0.
    • Mark (r, c) as visited.
    • Try all 4 directions and take:
      • min(1 + DFS(neighbor))
    • Unmark (r, c) (backtrack).
    • Return the minimum found.
  4. Update grid[r][c] with the returned minimum distance.
class Solution:
    def islandsAndTreasure(self, grid: List[List[int]]) -> None:
        ROWS, COLS = len(grid), len(grid[0])
        directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
        INF = 2147483647
        visit = [[False for _ in range(COLS)] for _ in range(ROWS)]

        def dfs(r, c):
            if (r < 0 or c < 0 or r >= ROWS or
                c >= COLS or grid[r][c] == -1 or
                visit[r][c]):
                return INF
            if grid[r][c] == 0:
                return 0

            visit[r][c] = True
            res = INF
            for dx, dy in directions:
                res = min(res, 1 + dfs(r + dx, c + dy))
            visit[r][c] = False
            return res

        for r in range(ROWS):
            for c in range(COLS):
                if grid[r][c] == INF:
                    grid[r][c] = dfs(r, c)

Time & Space Complexity

  • Time complexity: O(mn4mn)O(m * n * 4 ^ {m * n})
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns in the gridgrid.


Intuition

BFS is perfect for shortest path in an unweighted grid.
From one empty cell (INF), we expand level-by-level (distance 0, 1, 2, ...). The first time we reach a treasure cell (0), we are guaranteed that distance is the minimum steps needed.

This approach runs a BFS separately for every INF cell, so it's simpler to think about, but can still be slow because many BFS runs repeat the same work.

Algorithm

  1. Define 4 directions (up, down, left, right) and let INF represent empty rooms.
  2. For each cell (r, c) in the grid:
    • If grid[r][c] == INF, run BFS(r, c) to find the nearest treasure distance.
    • Replace grid[r][c] with that returned distance.
  3. BFS(sr, sc):
    • Initialize a queue with (sr, sc) and a visited[ROWS][COLS].
    • Set steps = 0.
    • While the queue is not empty:
      • Process exactly the current queue size (one BFS "layer").
      • If any popped cell is a treasure (grid[row][col] == 0), return steps.
      • Otherwise, push valid neighbors (in bounds, not visited, not a wall (-1)) into the queue.
      • After finishing the layer, increment steps.
    • If BFS ends without finding a treasure, return INF (no reachable treasure).
class Solution:
    def islandsAndTreasure(self, grid: List[List[int]]) -> None:
        ROWS, COLS = len(grid), len(grid[0])
        directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
        INF = 2147483647

        def bfs(r, c):
            q = deque([(r, c)])
            visit = [[False] * COLS for _ in range(ROWS)]
            visit[r][c] = True
            steps = 0
            while q:
                for _ in range(len(q)):
                    row, col = q.popleft()
                    if grid[row][col] == 0:
                        return steps
                    for dr, dc in directions:
                        nr, nc = row + dr, col + dc
                        if (0 <= nr < ROWS and 0 <= nc < COLS and
                            not visit[nr][nc] and grid[nr][nc] != -1
                        ):
                            visit[nr][nc] = True
                            q.append((nr, nc))
                steps += 1
            return INF

        for r in range(ROWS):
            for c in range(COLS):
                if grid[r][c] == INF:
                    grid[r][c] = bfs(r, c)

Time & Space Complexity

  • Time complexity: O((mn)2)O((m * n) ^ 2)
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns in the gridgrid.


3. Multi Source BFS

Intuition

Instead of running BFS from every empty room, run BFS once starting from all treasures (0 cells) at the same time.

Why this works:

  • BFS spreads out in "waves" of distance 0, 1, 2, ...
  • If we start the queue with all treasures, the first time the wave reaches an empty cell, it must be from the closest treasure (because BFS guarantees the first visit is the shortest distance in an unweighted grid).
    So each cell gets filled with its minimum distance to any treasure.

This avoids repeated work and is the optimal approach.

Algorithm

  1. Put all treasure cells (grid[r][c] == 0) into a queue.
  2. Mark them visited. Walls (-1) are never added.
  3. Set dist = 0.
  4. While the queue is not empty:
    • Process all nodes currently in the queue (this is one BFS level = distance dist).
    • For each cell popped:
      • Set grid[r][c] = dist (distance to nearest treasure).
      • Try its 4 neighbors:
        • If in bounds, not a wall, and not visited -> mark visited and push to queue.
    • After finishing the level, do dist += 1.
  5. After BFS ends, every reachable empty room has been updated with its shortest distance; unreachable rooms remain INF.
class Solution:
    def islandsAndTreasure(self, grid: List[List[int]]) -> None:
        ROWS, COLS = len(grid), len(grid[0])
        visit = set()
        q = deque()

        def addCell(r, c):
            if (min(r, c) < 0 or r == ROWS or c == COLS or
                (r, c) in visit or grid[r][c] == -1
            ):
                return
            visit.add((r, c))
            q.append([r, c])

        for r in range(ROWS):
            for c in range(COLS):
                if grid[r][c] == 0:
                    q.append([r, c])
                    visit.add((r, c))

        dist = 0
        while q:
            for i in range(len(q)):
                r, c = q.popleft()
                grid[r][c] = dist
                addCell(r + 1, c)
                addCell(r - 1, c)
                addCell(r, c + 1)
                addCell(r, c - 1)
            dist += 1

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns in the gridgrid.


Common Pitfalls

Starting BFS from Empty Rooms Instead of Treasures

A common mistake is to run BFS from each empty room to find the nearest treasure, resulting in O((m*n)^2) time complexity. The optimal approach is multi-source BFS starting from all treasures simultaneously, which processes each cell exactly once.

Not Distinguishing Walls from Unvisited Cells

Walls are represented by -1 and should never be added to the queue or updated. Confusing the wall value with the infinity value (2147483647) for empty rooms can cause incorrect distance calculations or infinite loops.

Updating Distance Before Adding to Queue

In BFS, the distance should be updated when a cell is first discovered (added to the queue), not when it is processed (removed from the queue). Updating too late can result in cells being added to the queue multiple times with different distances, leading to incorrect results and inefficiency.