LeetCode 490 - The Maze
This problem asks us to determine whether a rolling ball can stop exactly at a destination cell inside a maze. The maze is represented as a two-dimensional grid where: - 0 represents an empty space the ball can roll through - 1 represents a wall The important detail is that…
Difficulty: 🟡 Medium
Topics: Array, Depth-First Search, Breadth-First Search, Matrix
Solution
Problem Understanding
This problem asks us to determine whether a rolling ball can stop exactly at a destination cell inside a maze.
The maze is represented as a two-dimensional grid where:
0represents an empty space the ball can roll through1represents a wall
The important detail is that the ball does not move one cell at a time. Instead, once the ball starts moving in a direction, it keeps rolling until it hits a wall. Only after stopping can it choose another direction.
This changes the problem significantly from a standard grid traversal problem. In a normal BFS or DFS on a matrix, we would move one step at a time. Here, every move continues until collision with a wall.
The input consists of:
maze, the gridstart, the starting coordinates of the balldestination, the target coordinates
The output should be:
trueif the ball can stop exactly at the destinationfalseotherwise
A critical subtlety is that merely passing through the destination is not enough. The ball must stop there. If the destination lies in the middle of a rolling path and the ball continues moving beyond it until a wall is reached, that path does not count.
The constraints tell us several important things:
- The maze dimensions are at most
100 x 100 - There can be up to
10,000cells total - A graph traversal solution is expected
- Exponential brute force approaches would be too slow
- We need to avoid revisiting states repeatedly
The problem guarantees that:
- Borders are walls, which simplifies rolling logic
- Start and destination are valid empty cells
- Start and destination are different
- There are at least two empty cells
Several edge cases are important:
- The destination may be reachable geometrically but impossible to stop on
- The maze may contain cycles, requiring visited tracking
- Long corridors can cause repeated rolling computations
- The ball may revisit the same stopping position from different paths
- A maze with many open spaces can create infinite traversal loops if visited states are not tracked correctly
Approaches
Brute Force Approach
A brute force solution would attempt every possible rolling sequence recursively without tracking visited stopping positions.
From every stopping point, we would:
- Roll in all four directions
- Continue until hitting a wall
- Recursively explore from the new stopping position
This approach is correct because it eventually explores every possible path the ball can take.
However, it is inefficient because the maze may contain cycles. The ball can revisit the same stopping position repeatedly through different paths. Without remembering previously explored states, the algorithm can loop indefinitely or perform massive redundant work.
For example, in an open maze, the ball might bounce between the same few stopping positions endlessly.
Optimal Approach
The key insight is that the only meaningful states are stopping positions.
The ball cannot make decisions while rolling. Decisions only occur after hitting a wall and stopping. Therefore, instead of treating every traversed cell as a state, we treat each stopping coordinate as a graph node.
This transforms the problem into a graph traversal problem:
- Each stopping position is a node
- A valid roll between two stopping positions is an edge
We can then use either DFS or BFS with a visited set to avoid revisiting stopping positions.
Whenever we process a position:
- Roll in all four directions
- Compute the next stopping position
- If that position has not been visited, explore it
This guarantees that every reachable stopping position is explored at most once.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | Exponential | Exponential | Repeatedly explores the same stopping states |
| Optimal DFS/BFS | O(m × n × max(m, n)) | O(m × n) | Visits each stopping position once |
Algorithm Walkthrough
We will use Breadth-First Search, although Depth-First Search works equally well.
Step 1: Initialize the traversal
Create:
- A queue for BFS
- A visited set or matrix
- The four movement directions
We begin by adding the starting position into the queue and marking it visited.
We track visited stopping positions because revisiting them provides no new information and can create infinite loops.
Step 2: Process positions level by level
While the queue is not empty:
- Remove the current stopping position
- Check whether it equals the destination
- If so, return
True
This means the ball can stop exactly at the destination.
Step 3: Roll in each direction
For every direction:
- Start from the current position
- Continue moving until the next move would hit a wall
- Stop at the last valid empty cell
This simulates the rolling behavior exactly as described in the problem.
The rolling loop is the core of the problem:
while within_bounds and next_cell_is_empty:
move_forward
The final position after this loop is a valid stopping point.
Step 4: Explore unvisited stopping points
If the stopping position has not been visited:
- Mark it visited
- Add it to the queue
This ensures each stopping node is processed only once.
Step 5: Finish traversal
If the queue becomes empty without reaching the destination, return False.
This means no sequence of valid rolls allows the ball to stop at the destination.
Why it works
The algorithm works because it systematically explores every reachable stopping position exactly once.
The invariant is:
Every position added to the queue is a valid stopping point reachable from the start.
Since every possible roll from every reachable stopping point is explored, the algorithm eventually examines all reachable states. If the destination is reachable as a stopping point, it will be discovered. If not, the traversal will terminate after exhausting all possibilities.
Python Solution
from collections import deque
from typing import List
class Solution:
def hasPath(
self,
maze: List[List[int]],
start: List[int],
destination: List[int]
) -> bool:
rows = len(maze)
cols = len(maze[0])
directions = [
(1, 0),
(-1, 0),
(0, 1),
(0, -1)
]
queue = deque([tuple(start)])
visited = set()
visited.add(tuple(start))
while queue:
row, col = queue.popleft()
if [row, col] == destination:
return True
for dr, dc in directions:
new_row = row
new_col = col
while (
0 <= new_row + dr < rows
and 0 <= new_col + dc < cols
and maze[new_row + dr][new_col + dc] == 0
):
new_row += dr
new_col += dc
if (new_row, new_col) not in visited:
visited.add((new_row, new_col))
queue.append((new_row, new_col))
return False
The implementation begins by computing the maze dimensions and defining the four movement directions.
A BFS queue stores stopping positions that still need exploration. The visited set prevents revisiting already explored stopping points.
For every dequeued position, the algorithm checks whether it matches the destination. If it does, the answer is immediately True.
The most important section is the rolling simulation. For each direction, the algorithm repeatedly advances the ball until the next step would either:
- Move outside the maze
- Hit a wall
The resulting coordinates represent the stopping point after rolling.
If this stopping position has not been visited before, it is added to the queue for future exploration.
Eventually, either the destination is found or all reachable stopping positions are exhausted.
Go Solution
package main
import "container/list"
func hasPath(maze [][]int, start []int, destination []int) bool {
rows := len(maze)
cols := len(maze[0])
directions := [][]int{
{1, 0},
{-1, 0},
{0, 1},
{0, -1},
}
queue := list.New()
queue.PushBack([]int{start[0], start[1]})
visited := make(map[[2]int]bool)
visited[[2]int{start[0], start[1]}] = true
for queue.Len() > 0 {
front := queue.Front()
queue.Remove(front)
position := front.Value.([]int)
row, col := position[0], position[1]
if row == destination[0] && col == destination[1] {
return true
}
for _, dir := range directions {
dr, dc := dir[0], dir[1]
newRow := row
newCol := col
for (
newRow+dr >= 0 &&
newRow+dr < rows &&
newCol+dc >= 0 &&
newCol+dc < cols &&
maze[newRow+dr][newCol+dc] == 0) {
newRow += dr
newCol += dc
}
key := [2]int{newRow, newCol}
if !visited[key] {
visited[key] = true
queue.PushBack([]int{newRow, newCol})
}
}
}
return false
}
The Go implementation follows the same logic as the Python solution, but there are some language-specific differences.
Go does not have a built-in deque structure like Python's collections.deque, so we use container/list as the BFS queue.
For visited tracking, Go maps cannot use slices as keys because slices are not comparable. Instead, we use fixed-size arrays of type [2]int.
The rolling logic remains identical to the Python version.
Since the constraints are small, integer overflow is not a concern.
Worked Examples
Example 1
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]
Initial State
| Variable | Value |
|---|---|
| Queue | [(0,4)] |
| Visited | {(0,4)} |
Process (0,4)
| Direction | Final Stop |
|---|---|
| Down | (2,4) |
| Up | (0,4) |
| Right | (0,4) |
| Left | (0,3) |
New queue contents:
| Queue | Visited |
|---|---|
[(2,4), (0,3)] |
{(0,4), (2,4), (0,3)} |
Process (2,4)
| Direction | Final Stop |
|---|---|
| Down | (2,4) |
| Up | (0,4) |
| Right | (2,4) |
| Left | (2,4) |
No new states added.
Process (0,3)
| Direction | Final Stop |
|---|---|
| Down | (1,3) |
| Up | (0,3) |
| Right | (0,4) |
| Left | (0,3) |
Queue becomes:
| Queue |
|---|
[(1,3)] |
The traversal continues exploring stopping positions until eventually reaching (4,4).
The algorithm returns True.
Example 2
destination = [3,2]
The ball can pass through (3,2) but cannot stop there because there is no wall configuration allowing the roll to terminate at that position.
The BFS explores all reachable stopping points and never dequeues (3,2).
The algorithm returns False.
Example 3
start = [4,3]
destination = [0,1]
The BFS explores all reachable stopping positions but never reaches (0,1) as a stopping point.
Even though nearby paths exist, the rolling mechanics prevent stopping exactly there.
The algorithm returns False.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(m × n × max(m, n)) | Each stopping position may roll across an entire row or column |
| Space | O(m × n) | Queue and visited set can store every cell |
The maze contains at most m × n stopping positions.
For each processed position, we attempt four rolls. A single roll can traverse up to max(m, n) cells in the worst case.
Therefore, the total time complexity is:
O(m × n × max(m, n))
The visited set and BFS queue together can contain at most all cells in the maze, giving:
O(m × n)
space complexity.
Test Cases
from typing import List
class Solution:
def hasPath(self, maze: List[List[int]], start: List[int], destination: List[int]) -> bool:
from collections import deque
rows = len(maze)
cols = len(maze[0])
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
queue = deque([tuple(start)])
visited = {tuple(start)}
while queue:
row, col = queue.popleft()
if [row, col] == destination:
return True
for dr, dc in directions:
nr, nc = row, col
while (
0 <= nr + dr < rows
and 0 <= nc + dc < cols
and maze[nr + dr][nc + dc] == 0
):
nr += dr
nc += dc
if (nr, nc) not in visited:
visited.add((nr, nc))
queue.append((nr, nc))
return False
solution = Solution()
# Example 1, reachable destination
assert solution.hasPath(
[[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]],
[0,4],
[4,4]
) == True
# Example 2, cannot stop at destination
assert solution.hasPath(
[[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]],
[0,4],
[3,2]
) == False
# Example 3, unreachable stopping point
assert solution.hasPath(
[[0,0,0,0,0],
[1,1,0,0,1],
[0,0,0,0,0],
[0,1,0,0,1],
[0,1,0,0,0]],
[4,3],
[0,1]
) == False
# Simple direct path
assert solution.hasPath(
[[0,0],
[0,0]],
[0,0],
[0,1]
) == True
# Destination passed through but not stoppable
assert solution.hasPath(
[[0,0,0]],
[0,0],
[0,1]
) == False
# Maze with cycles
assert solution.hasPath(
[[0,0,0],
[0,1,0],
[0,0,0]],
[0,0],
[2,2]
) == True
# Single corridor
assert solution.hasPath(
[[0,0,0,0,0]],
[0,0],
[0,4]
) == True
# Blocked destination
assert solution.hasPath(
[[0,1,0],
[0,1,0],
[0,0,0]],
[0,0],
[0,2]
) == False
print("All tests passed.")
| Test | Why |
|---|---|
| Example 1 | Valid reachable stopping path |
| Example 2 | Destination can be crossed but not stopped on |
| Example 3 | Unreachable stopping position |
| Simple direct path | Basic movement correctness |
| Pass-through destination | Verifies stopping logic |
| Maze with cycles | Ensures visited tracking works |
| Single corridor | Tests long rolling behavior |
| Blocked destination | Tests disconnected regions |
Edge Cases
Destination is passed through but cannot be stopped on
One of the trickiest aspects of this problem is that the ball may roll through the destination without stopping there.
For example:
[0,0,0]
Starting from the leftmost cell and rolling right causes the ball to stop only at the far-right wall boundary. The middle cell is crossed but never becomes a stopping point.
A naive implementation that checks whether the ball ever touches the destination during rolling would incorrectly return True.
This implementation only checks positions after rolling completely stops, which correctly handles this case.
Cycles in the maze
A maze may allow the ball to revisit the same stopping positions repeatedly.
Without a visited set, DFS or BFS could loop forever.
For example:
0 0 0
0 1 0
0 0 0
The ball can repeatedly cycle around the central wall.
The implementation prevents infinite traversal by marking each stopping position visited immediately after discovery.
Long uninterrupted corridors
A maze may contain long rows or columns with no internal walls.
In these cases, the rolling simulation must continue correctly until the final stopping point.
A common bug is stopping one step too early or overshooting the wall boundary.
This implementation avoids those mistakes by checking whether the next cell is valid before advancing:
while next_position_is_valid:
move_forward
As a result, the ball always stops at the correct final empty cell before the wall.