โ† Back to DevBytes

Solving Walls and Gates in Python: Step-by-Step Guide

Introduction to Walls and Gates

The Walls and Gates problem is a classic graph traversal challenge frequently encountered in coding interviews and competitive programming. Given a 2D grid representing a building's floor plan, you must fill each empty room with the distance to its nearest gate. Walls block movement, and unreachable rooms remain at their initial infinite value.

This problem is an excellent test of your understanding of Breadth-First Search (BFS) and multi-source traversal techniques. In this tutorial, we will break down the problem, explore multiple solution strategies, and implement an efficient Python solution step by step.

What Is the Walls and Gates Problem?

Imagine a 2D matrix where each cell can be one of three values:

Movement is allowed in four directions: up, down, left, and right. The goal is to update each empty room cell with the shortest distance to the nearest gate. If a room cannot reach any gate, it should remain INF.

Example Input and Output

Consider the following grid:

INF  -1   0   INF
INF INF INF  -1
INF  -1 INF  -1
  0  -1 INF INF

After processing, the grid should become:

  3  -1   0    1
  2   2   1   -1
  1  -1   2   -1
  0  -1   3    4

Each empty room now holds the shortest distance to the nearest gate, while walls and gates remain unchanged.

Why This Problem Matters

The Walls and Gates problem is more than an interview exercise. It models real-world scenarios such as:

Mastering this problem teaches you multi-source BFS, a technique that scales to many graph problems where you start from several origins simultaneously.

Understanding the Solution Strategy

Why Breadth-First Search?

BFS is the natural choice for shortest-path problems on unweighted graphs. Each step in BFS corresponds to moving one cell, so the first time we reach a room, we have found the shortest distance to it from a gate.

A naive approach would run BFS from each gate individually and update rooms with the minimum distance found. However, this wastes computation by revisiting rooms multiple times. A more efficient approach uses multi-source BFS, where all gates are added to the queue at once and processed level by level.

Multi-Source BFS Explained

Instead of starting from a single source, we enqueue all gates simultaneously. Each gate acts as a source at distance 0. As we expand outward, every room we discover is guaranteed to be at its shortest distance from some gate, because BFS explores in order of increasing distance.

This approach ensures each cell is visited exactly once, giving us an optimal time complexity.

Step-by-Step Implementation

Let us build the solution incrementally. We will use a collections.deque for efficient queue operations.

Step 1: Setting Up the Function Signature

We define a function that modifies the grid in place:

from collections import deque
from typing import List

def walls_and_gates(rooms: List[List[int]]) -> None:
    """
    Fill each empty room with the distance to its nearest gate.
    Modifies the grid in place.
    """
    if not rooms or not rooms[0]:
        return

We handle the edge case of an empty grid immediately to avoid index errors.

Step 2: Identifying Dimensions and Directions

Next, we capture the grid dimensions and define the four possible movement directions:

    rows, cols = len(rooms), len(rooms[0])
    
    directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]

The directions list represents up, down, left, and right movements. Using a list of tuples keeps the code clean and easy to iterate over.

Step 3: Initializing the Queue with All Gates

We scan the entire grid and enqueue every gate's coordinates:

    queue = deque()
    
    for r in range(rows):
        for c in range(cols):
            if rooms[r][c] == 0:
                queue.append((r, c))

By adding all gates at once, we set up the multi-source BFS. Each gate starts at distance 0, and the BFS will expand outward from all of them simultaneously.

Step 4: Running the BFS

Now we process the queue level by level:

    while queue:
        row, col = queue.popleft()
        
        for dr, dc in directions:
            new_row, new_col = row + dr, col + dc
            
            # Check bounds
            if 0 <= new_row < rows and 0 <= new_col < cols:
                # Only process unvisited empty rooms
                if rooms[new_row][new_col] == 2147483647:
                    rooms[new_row][new_col] = rooms[row][col] + 1
                    queue.append((new_row, new_col))

The key insight is the condition rooms[new_row][new_col] == 2147483647. We only process cells that are still marked as INF, meaning they have not been visited. Since BFS explores in order of distance, the first time we reach a room is via the shortest path, so we can safely set its distance and never revisit it.

Step 5: The Complete Solution

Putting it all together, here is the complete implementation:

from collections import deque
from typing import List

def walls_and_gates(rooms: List[List[int]]) -> None:
    """
    Fill each empty room with the distance to its nearest gate.
    Modifies the grid in place.
    
    Args:
        rooms: 2D grid where -1 is a wall, 0 is a gate,
               and 2147483647 is an empty room.
    """
    if not rooms or not rooms[0]:
        return
    
    INF = 2147483647
    rows, cols = len(rooms), len(rooms[0])
    directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
    
    # Initialize queue with all gate positions
    queue = deque()
    for r in range(rows):
        for c in range(cols):
            if rooms[r][c] == 0:
                queue.append((r, c))
    
    # Multi-source BFS
    while queue:
        row, col = queue.popleft()
        
        for dr, dc in directions:
            new_row, new_col = row + dr, col + dc
            
            if (0 <= new_row < rows and 
                0 <= new_col < cols and 
                rooms[new_row][new_col] == INF):
                rooms[new_row][new_col] = rooms[row][col] + 1
                queue.append((new_row, new_col))

Step 6: Testing the Solution

Let us verify the solution with the example from earlier:

INF = 2147483647

rooms = [
    [INF, -1,  0,  INF],
    [INF, INF, INF, -1],
    [INF, -1,  INF, -1],
    [  0, -1,  INF, INF]
]

walls_and_gates(rooms)

for row in rooms:
    print([str(cell).rjust(4) for cell in row])

Output:

['   3', '  -1', '   0', '   1']
['   2', '   2', '   1', '  -1']
['   1', '  -1', '   2', '  -1']
['   0', '  -1', '   3', '   4']

The output matches our expected result, confirming the solution works correctly.

Alternative Approach: Depth-First Search

While BFS is the optimal approach, you can also solve this problem using DFS. The idea is to start from each gate and recursively explore neighboring rooms, updating distances when a shorter path is found.

from typing import List

def walls_and_gates_dfs(rooms: List[List[int]]) -> None:
    if not rooms or not rooms[0]:
        return
    
    INF = 2147483647
    rows, cols = len(rooms), len(rooms[0])
    directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
    
    def dfs(row: int, col: int, distance: int) -> None:
        # Skip if out of bounds or not an improvement
        if (row < 0 or row >= rows or 
            col < 0 or col >= cols or 
            rooms[row][col] < distance):
            return
        
        rooms[row][col] = distance
        
        for dr, dc in directions:
            dfs(row + dr, col + dc, distance + 1)
    
    for r in range(rows):
        for c in range(cols):
            if rooms[r][c] == 0:
                dfs(r, c, 0)

The DFS approach works but has a significant drawback: it may revisit cells multiple times when a shorter path is discovered later. This leads to a worst-case time complexity of O((m*n)^2) compared to BFS's O(m*n). Use DFS only when you need to understand all possible paths or when the grid is small.

Complexity Analysis

BFS Solution

DFS Solution

For production code and interviews, the BFS approach is strongly preferred due to its guaranteed linear time complexity.

Best Practices

Use Multi-Source BFS

Always enqueue all gates at the start rather than running separate BFS from each gate. This ensures each cell is processed exactly once, giving optimal performance.

Modify In Place When Possible

The grid itself can serve as a visited tracker. By checking whether a cell still holds INF, you avoid allocating a separate visited set, reducing memory usage.

Define INF as a Named Constant

Instead of hardcoding 2147483647 throughout your code, define it as a constant. This improves readability and makes the code easier to maintain:

INF = 2147483647

Validate Input Early

Always check for empty grids at the start of your function. This prevents runtime errors and makes your code more robust:

if not rooms or not rooms[0]:
    return

Use Deque for Queue Operations

Python's collections.deque provides O(1) append and pop operations from both ends. Using a regular list with pop(0) would result in O(n) operations, significantly degrading performance.

Consider Edge Cases

Always test your solution against these scenarios:

Common Mistakes to Avoid

Using DFS Without Distance Checks

A naive DFS that does not check whether the current distance is shorter than the existing value will loop infinitely or produce incorrect results. Always include the condition rooms[row][col] < distance to prune unnecessary recursive calls.

Forgetting Boundary Checks

Always verify that new coordinates are within the grid bounds before accessing the grid. Failing to do so results in IndexError exceptions.

Processing Walls and Gates as Rooms

Ensure your BFS only processes cells marked INF. Walls (-1) and gates (0) should never be enqueued or overwritten. The check rooms[new_row][new_col] == INF handles this naturally.

Extending the Problem

Once you understand the core solution, consider these variations to deepen your skills:

Conclusion

The Walls and Gates problem is a powerful demonstration of multi-source BFS in action. By enqueuing all gates simultaneously and processing cells level by level, we achieve an elegant O(m*n) solution that visits each cell exactly once. The key takeaways are recognizing when BFS is appropriate for shortest-path problems, leveraging the grid itself as a visited tracker, and understanding why multi-source initialization outperforms running separate traversals. With the implementation and best practices covered in this guide, you are well equipped to solve this problem confidently in interviews and apply the same techniques to a wide range of graph traversal challenges.

๐Ÿ›  Tools from DevBytes

Inventory Tracker Pro โ€” Excel inventory system, low-stock alerts ยท $19
AI Dev Kit for Mac โ€” local AI dev environment templates ยท $9.99
KeyMapper for Mac โ€” custom keyboard shortcut toolkit ยท $7.99

โ† Back to all articles