Facebook Pixel

505. The Maze II πŸ”’

Problem Description

You have a maze represented as a 2D grid where:

  • 0 represents an empty space (the ball can pass through)
  • 1 represents a wall (the ball cannot pass through)

A ball is placed at a starting position in the maze. The ball follows these movement rules:

  1. The ball can roll in four directions: up, down, left, or right
  2. Once the ball starts rolling in a direction, it keeps rolling until it hits a wall
  3. When the ball hits a wall, it stops at the last empty space before the wall
  4. After stopping, the ball can choose a new direction to roll

Given:

  • A maze of size m x n
  • A starting position start = [start_row, start_col]
  • A destination position destination = [destination_row, destination_col]

Your task is to find the shortest distance (measured in number of empty spaces traveled) for the ball to reach the destination position. The distance count:

  • Excludes the starting position
  • Includes the destination position
  • Counts every empty space the ball rolls through

If it's impossible for the ball to stop at the destination, return -1.

Important Note: The borders of the maze are considered walls, so the ball will always stop when reaching the edge of the maze.

Example: If a ball starts at position [0,4] and rolls left, passing through positions [0,3], [0,2], [0,1] before hitting a wall at column 0, the ball stops at [0,1] and has traveled a distance of 3.

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 maze can be viewed as a graph where each empty cell is a node, and edges exist between cells that the ball can travel between (following the rolling rules).

Is it a tree?

  • No: The maze graph contains cycles - the ball can potentially return to previously visited positions by taking different paths.

Is the problem related to directed acyclic graphs?

  • No: The graph representation of possible ball movements contains cycles, not a DAG.

Is the problem related to shortest paths?

  • Yes: We need to find the shortest distance (minimum number of spaces traveled) for the ball to reach the destination.

Is the graph weighted?

  • Yes: Each edge has a weight representing the distance traveled when the ball rolls from one stopping point to another. When the ball rolls from one position until it hits a wall, the weight is the number of spaces it travels.

Dijkstra's Algorithm

  • Yes: Since we have a weighted graph and need to find the shortest path, Dijkstra's algorithm is appropriate.

Conclusion: The flowchart suggests using Dijkstra's Algorithm for this problem. However, the provided solution uses a modified BFS approach that tracks distances, which is essentially implementing Dijkstra's algorithm concept. The solution maintains a distance matrix dist to track the shortest distance to reach each position, and uses a queue to explore positions in order of their distances. This is why the solution uses DFS-like exploration (rolling until hitting a wall) combined with BFS queue management to find the shortest path in this weighted graph scenario.

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

Intuition

The key insight is recognizing that this isn't a standard maze problem where we move one cell at a time. The ball's rolling behavior creates a unique challenge: once it starts rolling, it continues until hitting a wall. This means we're not exploring individual cells, but rather stopping points where the ball comes to rest.

Think of it this way: instead of a grid where we can move to any adjacent cell, we have a graph where nodes are "valid stopping positions" and edges represent "rolling paths" between these positions. The weight of each edge is the number of cells traveled during that roll.

Why can't we use simple BFS? In standard BFS, we assume each move has the same cost (usually 1). But here, rolling from one stopping point to another can cover different distances. Rolling left might travel 3 cells before hitting a wall, while rolling down might travel 5 cells. These different distances make it a weighted graph problem.

The natural approach is to track the minimum distance to reach each possible stopping position. We start from the initial position with distance 0, then explore all four directions:

  1. Roll in each direction until hitting a wall
  2. Calculate the total distance to reach that new stopping point
  3. If this distance is better than what we've found before, update it and continue exploring from there

This is essentially Dijkstra's algorithm in disguise. We're maintaining a distance array dist[i][j] that stores the shortest distance to reach position (i, j) as a stopping point. We keep exploring positions, always updating distances when we find a shorter path, until we've explored all reachable positions.

The beauty of this approach is that it guarantees we find the shortest path because we're systematically exploring all possible rolling sequences while keeping track of the minimum distance to each stopping point.

Learn more about Depth-First Search, Breadth-First Search, Graph, Shortest Path and Heap (Priority Queue) patterns.

Solution Approach

The implementation uses a modified BFS with distance tracking to find the shortest path. Let's walk through the key components:

Data Structures:

  • deque: A queue to store positions to be explored
  • dist: A 2D array initialized with inf to track the minimum distance to reach each cell as a stopping point

Initialization:

dist[si][sj] = 0  # Starting position has distance 0
q = deque([(si, sj)])  # Add start position to queue

Main Algorithm:

  1. Process each position from the queue:

    • Pop a position (i, j) from the queue
    • This represents a stopping point where the ball has come to rest
  2. Explore all four directions using pairwise(dirs):

    • dirs = (-1, 0, 1, 0, -1) cleverly encodes the four directions
    • pairwise(dirs) generates pairs: (-1, 0), (0, 1), (1, 0), (0, -1) representing up, right, down, left
  3. Simulate rolling in each direction:

    x, y, k = i, j, dist[i][j]  # Start from current position with current distance
    while 0 <= x + a < m and 0 <= y + b < n and maze[x + a][y + b] == 0:
        x, y, k = x + a, y + b, k + 1
    • Keep rolling while the next cell is valid and empty
    • Increment distance k for each cell traveled
    • Stop when hitting a wall or boundary
  4. Update distance if a shorter path is found:

    if k < dist[x][y]:
        dist[x][y] = k
        q.append((x, y))
    • Only update and re-explore if we found a better path
    • This ensures we don't get stuck in infinite loops
  5. Return the result:

    • Check dist[di][dj] for the destination
    • Return -1 if it's still inf (unreachable)
    • Otherwise return the shortest distance found

Why this works:

The algorithm essentially performs a relaxation process similar to Dijkstra's algorithm. By only adding positions to the queue when we find a shorter path, we ensure that:

  • Each position is processed with increasingly better distances
  • We eventually find the optimal distance to all reachable positions
  • The destination's distance converges to the minimum possible value

The key insight is that we're not tracking distances to every cell, but only to cells where the ball can stop (after rolling and hitting a wall). This significantly reduces the search space while still guaranteeing the shortest path.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Given:

maze = [[0,0,1,0,0],
        [0,0,0,0,0],
        [0,0,0,1,0],
        [1,1,0,1,1],
        [0,0,0,0,0]]
      
start = [0,4]
destination = [4,4]

Step-by-step execution:

Initialization:

  • Create dist array (5x5) filled with inf
  • Set dist[0][4] = 0 (starting position)
  • Queue: [(0,4)]

Round 1: Process position (0,4)

  • Current distance: 0
  • Try all four directions:
    • Up: Can't move (boundary)
    • Right: Can't move (boundary)
    • Down: Roll from (0,4) β†’ (1,4) β†’ (2,4) β†’ stop at (2,4) (hits wall at row 3)
      • Distance: 0 + 2 = 2
      • Update dist[2][4] = 2, add (2,4) to queue
    • Left: Roll from (0,4) β†’ (0,3) β†’ (0,2) β†’ stop at (0,1) (hits wall at column 2)
      • Distance: 0 + 3 = 3
      • Update dist[0][1] = 3, add (0,1) to queue
  • Queue: [(2,4), (0,1)]

Round 2: Process position (2,4)

  • Current distance: 2
  • Try all four directions:
    • Up: Roll back to (0,4) - distance 4, but dist[0][4] = 0 < 4, skip
    • Right: Can't move (boundary)
    • Down: Can't move (wall immediately below)
    • Left: Roll from (2,4) β†’ (2,3) β†’ (2,2) β†’ (2,1) β†’ stop at (2,0) (hits boundary)
      • Distance: 2 + 4 = 6
      • Update dist[2][0] = 6, add (2,0) to queue
  • Queue: [(0,1), (2,0)]

Round 3: Process position (0,1)

  • Current distance: 3
  • Try all four directions:
    • Up: Can't move (boundary)
    • Right: Roll back to (0,4) - distance 6, but dist[0][4] = 0 < 6, skip
    • Down: Roll from (0,1) β†’ (1,1) β†’ (2,1) β†’ stop at (2,1) (hits wall at row 3)
      • Distance: 3 + 2 = 5
      • Update dist[2][1] = 5, add (2,1) to queue
    • Left: Roll to (0,0) (hits boundary)
      • Distance: 3 + 1 = 4
      • Update dist[0][0] = 4, add (0,0) to queue
  • Queue: [(2,0), (2,1), (0,0)]

Continue processing...

After several more rounds, we eventually reach position (4,4) through the following path:

  • (0,4) β†’ (2,4) [distance: 2]
  • (2,4) β†’ (2,0) [distance: 6]
  • (2,0) β†’ (4,0) [distance: 8]
  • (4,0) β†’ (4,4) [distance: 12]

Final Result: dist[4][4] = 12

The algorithm finds that the shortest distance from start (0,4) to destination (4,4) is 12 empty spaces traveled. Note how the ball must take a winding path due to the walls, and how we only track distances to positions where the ball can actually stop (after hitting a wall).

Solution Implementation

1class Solution:
2    def shortestDistance(
3        self, maze: List[List[int]], start: List[int], destination: List[int]
4    ) -> int:
5        # Get maze dimensions
6        rows, cols = len(maze), len(maze[0])
7      
8        # Direction vectors: up, right, down, left
9        directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
10      
11        # Extract start and destination coordinates
12        start_row, start_col = start
13        dest_row, dest_col = destination
14      
15        # Initialize BFS queue with starting position
16        queue = deque([(start_row, start_col)])
17      
18        # Initialize distance matrix with infinity
19        distance = [[float('inf')] * cols for _ in range(rows)]
20        distance[start_row][start_col] = 0
21      
22        # BFS to find shortest path
23        while queue:
24            current_row, current_col = queue.popleft()
25          
26            # Try rolling in each direction
27            for delta_row, delta_col in directions:
28                # Initialize rolling position and step counter
29                next_row = current_row
30                next_col = current_col
31                steps = distance[current_row][current_col]
32              
33                # Keep rolling until hitting a wall or boundary
34                while (0 <= next_row + delta_row < rows and 
35                       0 <= next_col + delta_col < cols and 
36                       maze[next_row + delta_row][next_col + delta_col] == 0):
37                    next_row += delta_row
38                    next_col += delta_col
39                    steps += 1
40              
41                # If we found a shorter path to this position, update it
42                if steps < distance[next_row][next_col]:
43                    distance[next_row][next_col] = steps
44                    queue.append((next_row, next_col))
45      
46        # Return the shortest distance to destination, or -1 if unreachable
47        return -1 if distance[dest_row][dest_col] == float('inf') else distance[dest_row][dest_col]
48
1class Solution {
2    public int shortestDistance(int[][] maze, int[] start, int[] destination) {
3        // Get maze dimensions
4        int rows = maze.length;
5        int cols = maze[0].length;
6      
7        // Initialize infinity value for unreachable cells
8        final int INFINITY = 1 << 30;
9      
10        // Create distance matrix to track shortest distance to each cell
11        int[][] distance = new int[rows][cols];
12        for (int[] row : distance) {
13            Arrays.fill(row, INFINITY);
14        }
15      
16        // Extract start and destination coordinates
17        int startRow = start[0], startCol = start[1];
18        int destRow = destination[0], destCol = destination[1];
19      
20        // Set starting point distance to 0
21        distance[startRow][startCol] = 0;
22      
23        // Initialize BFS queue with starting position
24        Deque<int[]> queue = new ArrayDeque<>();
25        queue.offer(new int[] {startRow, startCol});
26      
27        // Direction vectors: up, right, down, left
28        int[] directions = {-1, 0, 1, 0, -1};
29      
30        // BFS to find shortest path
31        while (!queue.isEmpty()) {
32            int[] current = queue.poll();
33            int currentRow = current[0];
34            int currentCol = current[1];
35          
36            // Try rolling the ball in all 4 directions
37            for (int dir = 0; dir < 4; dir++) {
38                // Initialize position and step counter
39                int nextRow = currentRow;
40                int nextCol = currentCol;
41                int steps = distance[currentRow][currentCol];
42              
43                // Get direction deltas
44                int rowDelta = directions[dir];
45                int colDelta = directions[dir + 1];
46              
47                // Keep rolling until hitting a wall or boundary
48                while (nextRow + rowDelta >= 0 && 
49                       nextRow + rowDelta < rows && 
50                       nextCol + colDelta >= 0 && 
51                       nextCol + colDelta < cols && 
52                       maze[nextRow + rowDelta][nextCol + colDelta] == 0) {
53                    nextRow += rowDelta;
54                    nextCol += colDelta;
55                    steps++;
56                }
57              
58                // Update distance if we found a shorter path
59                if (steps < distance[nextRow][nextCol]) {
60                    distance[nextRow][nextCol] = steps;
61                    queue.offer(new int[] {nextRow, nextCol});
62                }
63            }
64        }
65      
66        // Return shortest distance to destination, or -1 if unreachable
67        return distance[destRow][destCol] == INFINITY ? -1 : distance[destRow][destCol];
68    }
69}
70
1class Solution {
2public:
3    int shortestDistance(vector<vector<int>>& maze, vector<int>& start, vector<int>& destination) {
4        // Get maze dimensions
5        int rows = maze.size();
6        int cols = maze[0].size();
7      
8        // Initialize distance array with a large value (infinity)
9        // 0x3f3f3f3f is commonly used as infinity in competitive programming
10        const int INF = 0x3f3f3f3f;
11        vector<vector<int>> distance(rows, vector<int>(cols, INF));
12      
13        // Extract start and destination coordinates
14        int startRow = start[0], startCol = start[1];
15        int destRow = destination[0], destCol = destination[1];
16      
17        // Distance to start position is 0
18        distance[startRow][startCol] = 0;
19      
20        // BFS queue to explore positions
21        queue<pair<int, int>> bfsQueue;
22        bfsQueue.emplace(startRow, startCol);
23      
24        // Direction vectors: up, right, down, left
25        // Using the trick where dirs[i] and dirs[i+1] form (dx, dy) pairs
26        int directions[5] = {-1, 0, 1, 0, -1};
27      
28        // Process all reachable positions using BFS
29        while (!bfsQueue.empty()) {
30            auto [currentRow, currentCol] = bfsQueue.front();
31            bfsQueue.pop();
32          
33            // Try rolling the ball in all 4 directions
34            for (int dir = 0; dir < 4; ++dir) {
35                // Initialize new position and step counter
36                int newRow = currentRow;
37                int newCol = currentCol;
38                int steps = distance[currentRow][currentCol];
39              
40                // Get direction deltas
41                int deltaRow = directions[dir];
42                int deltaCol = directions[dir + 1];
43              
44                // Keep rolling until hitting a wall or boundary
45                while (newRow + deltaRow >= 0 && 
46                       newRow + deltaRow < rows && 
47                       newCol + deltaCol >= 0 && 
48                       newCol + deltaCol < cols && 
49                       maze[newRow + deltaRow][newCol + deltaCol] == 0) {
50                    newRow += deltaRow;
51                    newCol += deltaCol;
52                    ++steps;
53                }
54              
55                // If we found a shorter path to this position, update it
56                if (steps < distance[newRow][newCol]) {
57                    distance[newRow][newCol] = steps;
58                    bfsQueue.emplace(newRow, newCol);
59                }
60            }
61        }
62      
63        // Return the shortest distance to destination, or -1 if unreachable
64        return distance[destRow][destCol] == INF ? -1 : distance[destRow][destCol];
65    }
66};
67
1/**
2 * Finds the shortest distance for a ball to roll from start to destination in a maze
3 * The ball can only stop when it hits a wall
4 * 
5 * @param maze - 2D array where 0 represents empty space and 1 represents wall
6 * @param start - Starting position [row, col]
7 * @param destination - Target position [row, col]
8 * @returns The shortest distance to reach destination, or -1 if impossible
9 */
10function shortestDistance(maze: number[][], start: number[], destination: number[]): number {
11    const rows = maze.length;
12    const cols = maze[0].length;
13  
14    // Initialize distance matrix with infinity for all positions
15    const distanceMatrix: number[][] = Array.from({ length: rows }, () =>
16        Array.from({ length: cols }, () => Infinity)
17    );
18  
19    // Extract start and destination coordinates
20    const [startRow, startCol] = start;
21    const [destRow, destCol] = destination;
22  
23    // Set starting position distance to 0
24    distanceMatrix[startRow][startCol] = 0;
25  
26    // Initialize BFS queue with starting position
27    const queue: number[][] = [[startRow, startCol]];
28  
29    // Direction vectors: up, right, down, left
30    const directions = [-1, 0, 1, 0, -1];
31  
32    // BFS to find shortest path
33    while (queue.length > 0) {
34        const [currentRow, currentCol] = queue.shift()!;
35      
36        // Try rolling the ball in all 4 directions
37        for (let dirIndex = 0; dirIndex < 4; dirIndex++) {
38            // Initialize position and step counter
39            let nextRow = currentRow;
40            let nextCol = currentCol;
41            let steps = distanceMatrix[currentRow][currentCol];
42          
43            // Get direction deltas
44            const rowDelta = directions[dirIndex];
45            const colDelta = directions[dirIndex + 1];
46          
47            // Keep rolling until hitting a wall or boundary
48            while (
49                nextRow + rowDelta >= 0 && 
50                nextRow + rowDelta < rows && 
51                nextCol + colDelta >= 0 && 
52                nextCol + colDelta < cols && 
53                maze[nextRow + rowDelta][nextCol + colDelta] === 0
54            ) {
55                nextRow += rowDelta;
56                nextCol += colDelta;
57                steps++;
58            }
59          
60            // Update distance if we found a shorter path
61            if (steps < distanceMatrix[nextRow][nextCol]) {
62                distanceMatrix[nextRow][nextCol] = steps;
63                queue.push([nextRow, nextCol]);
64            }
65        }
66    }
67  
68    // Return the shortest distance to destination, or -1 if unreachable
69    return distanceMatrix[destRow][destCol] === Infinity ? -1 : distanceMatrix[destRow][destCol];
70}
71

Time and Space Complexity

Time Complexity: O(m * n * max(m, n))

The algorithm uses BFS with a deque to explore the maze. In the worst case:

  • Each cell (i, j) in the maze can be visited multiple times (when a shorter distance is found)
  • There are m * n cells total
  • For each cell, we explore 4 directions
  • In each direction, the ball rolls until it hits a wall, which takes at most max(m, n) steps
  • While a cell could theoretically be added to the queue multiple times, the distance check if k < dist[x][y] ensures we only process improving paths
  • In the worst case, each cell could be updated O(m * n) times (once from each other cell)

Therefore, the overall time complexity is O(m * n * max(m, n)).

Space Complexity: O(m * n)

The space is used for:

  • The dist matrix: O(m * n) to store the minimum distance to each cell
  • The deque q: In the worst case, it could contain all cells, which is O(m * n)
  • Other variables like dirs and loop variables: O(1)

The total space complexity is O(m * n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Confusing Cell Distance vs. Path Distance

The Problem: A common mistake is treating this like a standard BFS where we track whether we've visited each cell. Students often write code like:

visited = set()
visited.add((start_row, start_col))
# ... in the loop:
if (next_row, next_col) not in visited:
    visited.add((next_row, next_col))
    queue.append((next_row, next_col))

This fails because the ball can reach the same stopping position via different paths with different distances. Using a simple visited set prevents us from finding shorter paths to the same position.

The Solution: Use a distance matrix instead of a visited set. Only re-process a position if we've found a shorter path to it:

if steps < distance[next_row][next_col]:
    distance[next_row][next_col] = steps
    queue.append((next_row, next_col))

Pitfall 2: Incorrect Rolling Simulation

The Problem: Students often make mistakes in the rolling logic, such as:

# Wrong: Checking the current position instead of the next
while 0 <= next_row < rows and 0 <= next_col < cols and maze[next_row][next_col] == 0:
    next_row += delta_row
    next_col += delta_col
    steps += 1

This causes the ball to roll through walls because we're checking if the current position is valid, not if the next position would be valid.

The Solution: Always check the NEXT position before moving:

while (0 <= next_row + delta_row < rows and 
       0 <= next_col + delta_col < cols and 
       maze[next_row + delta_row][next_col + delta_col] == 0):
    next_row += delta_row
    next_col += delta_col
    steps += 1

Pitfall 3: Not Understanding When the Ball Can Stop

The Problem: A critical misunderstanding is thinking the ball can stop at any empty cell. Students might check if the ball passes through the destination:

# Wrong approach
while rolling:
    if (next_row, next_col) == (dest_row, dest_col):
        return steps  # Can't just stop here!

The ball can ONLY stop when it hits a wall or boundary. It cannot stop in the middle of an empty corridor just because it reached the destination.

The Solution: Let the ball complete its roll in each direction, then check if the stopping position matches the destination:

# Roll until hitting a wall
while (can_continue_rolling):
    # keep rolling
  
# After stopping, check if this is a valid stopping position
if steps < distance[next_row][next_col]:
    # Process this stopping position

Pitfall 4: Initializing Distance Incorrectly

The Problem: Forgetting to set the starting position's distance to 0:

distance = [[float('inf')] * cols for _ in range(rows)]
# Forgot: distance[start_row][start_col] = 0

This causes the algorithm to never find valid paths because all distances remain infinite.

The Solution: Always initialize the starting position's distance to 0:

distance = [[float('inf')] * cols for _ in range(rows)]
distance[start_row][start_col] = 0  # Critical initialization
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

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

Load More