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:
- The ball can roll in four directions: up, down, left, or right
- Once the ball starts rolling in a direction, it keeps rolling until it hits a wall
- When the ball hits a wall, it stops at the last empty space before the wall
- 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.
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:
- Roll in each direction until hitting a wall
- Calculate the total distance to reach that new stopping point
- 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 exploreddist
: A 2D array initialized withinf
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:
-
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
- Pop a position
-
Explore all four directions using
pairwise(dirs)
:dirs = (-1, 0, 1, 0, -1)
cleverly encodes the four directionspairwise(dirs)
generates pairs:(-1, 0)
,(0, 1)
,(1, 0)
,(0, -1)
representing up, right, down, left
-
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
-
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
-
Return the result:
- Check
dist[di][dj]
for the destination - Return
-1
if it's stillinf
(unreachable) - Otherwise return the shortest distance found
- Check
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 EvaluatorExample 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 withinf
- 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
- Up: Roll back to (0,4) - distance 4, but
- 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 isO(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
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
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
Want a Structured Path to Master System Design Too? Donβt Miss This!