490. The Maze π
Problem Description
You have a maze represented as a 2D grid where:
0
represents an empty space1
represents a wall
A ball is placed in this maze at a starting position. The ball follows these movement rules:
- It 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 and can then 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]
You need to determine if it's possible for the ball to stop at the destination position.
Important notes:
- The ball must stop at the destination (not just pass through it)
- The borders of the maze are considered walls
- The ball can only stop when it hits a wall
For example, if the ball is rolling right, it will continue moving right through all empty spaces (0
) until it reaches a wall (1
) or the maze boundary, at which point it stops and can choose a new direction.
The function should return true
if the ball can reach and stop at the destination, false
otherwise.
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 represented as a graph where each empty cell (0) is a node, and edges exist between cells that the ball can roll between. The ball's movement creates paths through this graph.
Is it a tree?
- No: The maze is not a tree structure. It's a 2D grid where multiple paths can exist between positions, and cycles are possible (the ball can return to previously visited positions through different routes).
Is the problem related to directed acyclic graphs?
- No: The maze graph can have cycles - the ball can potentially return to a position it has already visited by taking a different path.
Is the problem related to shortest paths?
- No: We don't need to find the shortest path. We only need to determine if the ball can reach and stop at the destination, regardless of the path length.
Does the problem involve connectivity?
- No: While we need to check if we can reach the destination, this isn't a pure connectivity problem like finding connected components or checking if nodes belong to the same component.
Does the problem have small constraints?
- Yes: Maze problems typically have reasonable constraints (usually up to 100x100 or similar), making DFS/backtracking feasible without timeout issues.
DFS/backtracking?
- Yes: DFS is perfect for exploring all possible paths the ball can take. We need to explore each possible direction the ball can roll from each stopping position until we either find the destination or exhaust all possibilities.
Conclusion: The flowchart suggests using DFS/backtracking to explore all possible paths the ball can take through the maze, checking if any path leads to stopping at the destination position.
Intuition
The key insight is understanding how the ball moves - it doesn't stop at every cell like typical maze problems. Once the ball starts rolling in a direction, it keeps going until it hits a wall. This creates a unique graph structure where nodes are not individual cells, but rather stopping positions.
Think of it this way: when the ball is at a position, we have at most 4 choices (up, down, left, right). For each direction, we simulate the ball rolling continuously until it hits a wall or boundary. The position where it stops becomes a new node in our search space.
This naturally leads to a DFS approach because:
- We need to explore all possible paths from the starting position
- From each stopping position, we branch out to explore all 4 directions
- We continue this exploration recursively until we either find the destination or exhaust all possibilities
The visited tracking is crucial here - we mark stopping positions as visited to avoid infinite loops. If the ball reaches a stopping position it has already visited, we know this path won't lead to a new solution.
Why DFS over BFS? Both would work, but DFS is simpler to implement for this problem. We don't need the shortest path (where BFS would shine), we just need to know if ANY path exists. DFS naturally explores one complete path before backtracking to try another, which is perfectly suited for this "can we reach the destination?" question.
The algorithm essentially builds a graph on-the-fly where:
- Nodes = stopping positions (where the ball hits a wall)
- Edges = the rolling motion between stopping positions
- Goal = check if destination node is reachable from start node
Solution Approach
The implementation uses DFS with a visited matrix to track which stopping positions we've already explored. Let's walk through the key components:
1. Visited Matrix Setup:
vis = [[False] * n for _ in range(m)]
We create a 2D boolean matrix matching the maze dimensions to track visited stopping positions. This prevents infinite loops when the ball revisits the same stopping point.
2. DFS Function Structure:
The dfs(i, j)
function explores all possible paths from position (i, j)
:
- First, it checks if this position was already visited. If yes, return immediately to avoid redundant exploration
- Mark the current position as visited:
vis[i][j] = True
- Check if we've reached the destination:
if [i, j] == destination
3. Rolling Simulation:
for a, b in [[0, -1], [0, 1], [1, 0], [-1, 0]]: x, y = i, j while 0 <= x + a < m and 0 <= y + b < n and maze[x + a][y + b] == 0: x, y = x + a, y + b dfs(x, y)
For each of the 4 directions (left, right, down, up):
- Initialize
(x, y)
to the current position - Use a while loop to simulate the ball rolling in direction
(a, b)
- The ball keeps rolling while:
- The next position
(x + a, y + b)
is within bounds - The next position is an empty space (value is 0)
- The next position
- When the loop exits,
(x, y)
is the stopping position - Recursively call
dfs(x, y)
to explore from this new stopping position
4. Main Execution:
dfs(start[0], start[1]) return vis[destination[0]][destination[1]]
- Start the DFS from the initial position
- After DFS completes, check if the destination was visited
- If
vis[destination[0]][destination[1]]
isTrue
, the ball can reach and stop at the destination
Why This Works: The algorithm systematically explores all reachable stopping positions from the start. By marking positions as visited, we ensure each stopping position is processed only once. If the destination becomes marked as visited during this exploration, it means there exists at least one valid path for the ball to reach and stop at the destination.
The time complexity is O(m*n*max(m,n))
where the extra factor accounts for the rolling simulation in each direction. The space complexity is O(m*n)
for the visited matrix and recursion stack.
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 how the DFS solution works.
Consider this maze:
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]
Where 0
= empty space, 1
= wall, S
= start position (0,4), D
= destination (4,4).
Visualized with S and D:
[0, 0, 1, 0, S] [0, 0, 0, 0, 0] [0, 0, 0, 1, 0] [1, 1, 0, 1, 1] [0, 0, 0, 0, D]
Step 1: Start DFS from position (0, 4)
- Mark (0, 4) as visited
- Try rolling in 4 directions:
Step 2: Rolling Left from (0, 4)
- Ball rolls: (0,4) β (0,3) β stops at (0,2) because maze[0][2] = 1 (wall)
- Call dfs(0, 2)
- Mark (0, 2) as visited, but it's not the destination
- From (0, 2), the ball can only roll down
Step 3: Rolling Down from (0, 2)
- Ball rolls: (0,2) β (1,2) β (2,2) β stops at (2,2) because maze[3][2] = 0 but maze[3][1] = 1 and maze[3][3] = 1 (surrounded by walls)
- Call dfs(2, 2)
- Mark (2, 2) as visited
Step 4: From (2, 2), try all directions
- Up: rolls back to (0, 2) - already visited, skip
- Down: can't move (wall at 3,2)
- Left: rolls to (2, 0) - stops at left boundary
- Right: rolls to (2, 3) - stops at wall
Step 5: Continue exploring from (2, 0)
- Mark (2, 0) as visited
- Roll down: goes to (4, 0)
- From (4, 0), roll right: (4,0) β (4,1) β (4,2) β (4,3) β (4,4)
- Ball stops at (4, 4) - this is our destination!
Step 6: Mark destination as visited
- vis[4][4] = True
- Return from recursive calls
Final Result:
After DFS completes, we check vis[4][4]
which is True
, so the function returns true
- the ball can reach the destination.
The key insight: The ball doesn't stop at every cell. When rolling from (4,0) to the right, it passes through (4,1), (4,2), and (4,3) without stopping, finally stopping at (4,4) when it hits the right boundary.
Solution Implementation
1class Solution:
2 def hasPath(
3 self, maze: List[List[int]], start: List[int], destination: List[int]
4 ) -> bool:
5 """
6 Determines if there's a path from start to destination in a maze where
7 a ball rolls until it hits a wall.
8
9 Args:
10 maze: 2D grid where 0 represents empty space and 1 represents wall
11 start: Starting position [row, col]
12 destination: Target position [row, col]
13
14 Returns:
15 True if a path exists, False otherwise
16 """
17
18 def dfs(row: int, col: int) -> None:
19 """
20 Depth-first search to explore all reachable positions.
21 The ball keeps rolling in a direction until it hits a wall.
22
23 Args:
24 row: Current row position
25 col: Current column position
26 """
27 # Skip if this position has already been visited
28 if visited[row][col]:
29 return
30
31 # Mark current position as visited
32 visited[row][col] = True
33
34 # Early termination if we reached the destination
35 if [row, col] == destination:
36 return
37
38 # Try rolling in all four directions: left, right, down, up
39 directions = [[0, -1], [0, 1], [1, 0], [-1, 0]]
40
41 for delta_row, delta_col in directions:
42 # Initialize position for rolling
43 next_row, next_col = row, col
44
45 # Keep rolling until hitting a wall or boundary
46 while (0 <= next_row + delta_row < rows and
47 0 <= next_col + delta_col < cols and
48 maze[next_row + delta_row][next_col + delta_col] == 0):
49 next_row += delta_row
50 next_col += delta_col
51
52 # Recursively explore from the stopped position
53 dfs(next_row, next_col)
54
55 # Initialize maze dimensions
56 rows, cols = len(maze), len(maze[0])
57
58 # Initialize visited matrix to track explored positions
59 visited = [[False] * cols for _ in range(rows)]
60
61 # Start DFS from the starting position
62 dfs(start[0], start[1])
63
64 # Check if destination was reached during exploration
65 return visited[destination[0]][destination[1]]
66
1class Solution {
2 // Visited array to track cells that have been explored
3 private boolean[][] visited;
4 // Reference to the maze grid
5 private int[][] maze;
6 // Destination coordinates
7 private int[] destination;
8 // Number of rows in the maze
9 private int rows;
10 // Number of columns in the maze
11 private int cols;
12
13 /**
14 * Determines if there's a path from start to destination in the maze.
15 * The ball rolls until it hits a wall, following physics rules.
16 *
17 * @param maze 2D array where 0 represents empty space and 1 represents wall
18 * @param start Starting position [row, col]
19 * @param destination Target position [row, col]
20 * @return true if a path exists, false otherwise
21 */
22 public boolean hasPath(int[][] maze, int[] start, int[] destination) {
23 // Initialize dimensions
24 this.rows = maze.length;
25 this.cols = maze[0].length;
26
27 // Store references
28 this.destination = destination;
29 this.maze = maze;
30
31 // Initialize visited array
32 this.visited = new boolean[rows][cols];
33
34 // Start DFS from the starting position
35 dfs(start[0], start[1]);
36
37 // Check if destination was reached
38 return visited[destination[0]][destination[1]];
39 }
40
41 /**
42 * Performs depth-first search to explore all possible stopping positions.
43 * The ball rolls in one direction until it hits a wall.
44 *
45 * @param row Current row position
46 * @param col Current column position
47 */
48 private void dfs(int row, int col) {
49 // Skip if this position has already been visited
50 if (visited[row][col]) {
51 return;
52 }
53
54 // Mark current position as visited
55 visited[row][col] = true;
56
57 // Early termination if destination is reached
58 if (row == destination[0] && col == destination[1]) {
59 return;
60 }
61
62 // Direction vectors: up, right, down, left
63 // Using pairs: (-1,0), (0,1), (1,0), (0,-1)
64 int[] directions = {-1, 0, 1, 0, -1};
65
66 // Try rolling the ball in all four directions
67 for (int k = 0; k < 4; k++) {
68 // Start from current position
69 int currentRow = row;
70 int currentCol = col;
71
72 // Get direction increments
73 int rowIncrement = directions[k];
74 int colIncrement = directions[k + 1];
75
76 // Keep rolling until hitting a wall or boundary
77 while (currentRow + rowIncrement >= 0 &&
78 currentRow + rowIncrement < rows &&
79 currentCol + colIncrement >= 0 &&
80 currentCol + colIncrement < cols &&
81 maze[currentRow + rowIncrement][currentCol + colIncrement] == 0) {
82
83 currentRow += rowIncrement;
84 currentCol += colIncrement;
85 }
86
87 // Recursively explore from the stopping position
88 dfs(currentRow, currentCol);
89 }
90 }
91}
92
1class Solution {
2public:
3 vector<vector<int>> maze;
4 vector<vector<bool>> visited;
5 vector<int> destination;
6 int rows;
7 int cols;
8
9 bool hasPath(vector<vector<int>>& maze, vector<int>& start, vector<int>& destination) {
10 // Initialize dimensions
11 rows = maze.size();
12 cols = maze[0].size();
13
14 // Store destination and maze as member variables
15 this->destination = destination;
16 this->maze = maze;
17
18 // Initialize visited array with all false values
19 visited.resize(rows, vector<bool>(cols, false));
20
21 // Start DFS from the starting position
22 dfs(start[0], start[1]);
23
24 // Check if destination was reached
25 return visited[destination[0]][destination[1]];
26 }
27
28private:
29 void dfs(int row, int col) {
30 // If already visited, return to avoid cycles
31 if (visited[row][col]) {
32 return;
33 }
34
35 // Mark current position as visited
36 visited[row][col] = true;
37
38 // If we reached the destination, no need to explore further
39 if (row == destination[0] && col == destination[1]) {
40 return;
41 }
42
43 // Direction vectors: up(-1,0), right(0,1), down(1,0), left(0,-1)
44 vector<int> directions = {-1, 0, 1, 0, -1};
45
46 // Try all four directions
47 for (int k = 0; k < 4; ++k) {
48 int nextRow = row;
49 int nextCol = col;
50 int deltaRow = directions[k];
51 int deltaCol = directions[k + 1];
52
53 // Keep rolling the ball in the current direction until hitting a wall
54 while (nextRow + deltaRow >= 0 && nextRow + deltaRow < rows &&
55 nextCol + deltaCol >= 0 && nextCol + deltaCol < cols &&
56 maze[nextRow + deltaRow][nextCol + deltaCol] == 0) {
57 nextRow += deltaRow;
58 nextCol += deltaCol;
59 }
60
61 // Recursively explore from the stopping position
62 dfs(nextRow, nextCol);
63 }
64 }
65};
66
1// Global variables to store maze state
2let maze: number[][];
3let visited: boolean[][];
4let destination: number[];
5let rows: number;
6let cols: number;
7
8function hasPath(mazeInput: number[][], start: number[], destinationInput: number[]): boolean {
9 // Initialize dimensions
10 rows = mazeInput.length;
11 cols = mazeInput[0].length;
12
13 // Store destination and maze as global variables
14 destination = destinationInput;
15 maze = mazeInput;
16
17 // Initialize visited array with all false values
18 visited = Array(rows).fill(null).map(() => Array(cols).fill(false));
19
20 // Start DFS from the starting position
21 dfs(start[0], start[1]);
22
23 // Check if destination was reached
24 return visited[destination[0]][destination[1]];
25}
26
27function dfs(row: number, col: number): void {
28 // If already visited, return to avoid cycles
29 if (visited[row][col]) {
30 return;
31 }
32
33 // Mark current position as visited
34 visited[row][col] = true;
35
36 // If we reached the destination, no need to explore further
37 if (row === destination[0] && col === destination[1]) {
38 return;
39 }
40
41 // Direction vectors: up(-1,0), right(0,1), down(1,0), left(0,-1)
42 const directions: number[] = [-1, 0, 1, 0, -1];
43
44 // Try all four directions
45 for (let k = 0; k < 4; k++) {
46 let nextRow: number = row;
47 let nextCol: number = col;
48 const deltaRow: number = directions[k];
49 const deltaCol: number = directions[k + 1];
50
51 // Keep rolling the ball in the current direction until hitting a wall
52 while (nextRow + deltaRow >= 0 && nextRow + deltaRow < rows &&
53 nextCol + deltaCol >= 0 && nextCol + deltaCol < cols &&
54 maze[nextRow + deltaRow][nextCol + deltaCol] === 0) {
55 nextRow += deltaRow;
56 nextCol += deltaCol;
57 }
58
59 // Recursively explore from the stopping position
60 dfs(nextRow, nextCol);
61 }
62}
63
Time and Space Complexity
Time Complexity: O(m * n * max(m, n))
The algorithm uses DFS to explore all reachable positions in the maze. In the worst case:
- We visit each cell at most once due to the visited array, giving us
O(m * n)
cells to explore - For each cell we visit, we simulate rolling the ball in 4 directions
- Each roll simulation can take up to
O(max(m, n))
steps in the worst case (rolling from one end of the maze to the other) - Therefore, the total time complexity is
O(m * n * max(m, n))
Space Complexity: O(m * n)
The space complexity consists of:
- The visited array
vis
which usesO(m * n)
space to track visited cells - The recursive call stack for DFS, which in the worst case can go up to
O(m * n)
depth if we visit all cells in a chain - Therefore, the total space complexity is
O(m * n)
Common Pitfalls
Pitfall 1: Confusing Visited Positions - Rolling vs. Stopping
The Problem: A common mistake is marking positions as visited while the ball is rolling through them, rather than only marking the positions where the ball stops. This leads to incorrect results because the ball might need to pass through the same position multiple times in different directions.
Incorrect Implementation:
def dfs(row, col):
if visited[row][col]:
return
visited[row][col] = True
for delta_row, delta_col in directions:
next_row, next_col = row, col
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
# WRONG: Marking intermediate positions as visited
visited[next_row][next_col] = True
dfs(next_row, next_col)
Why It's Wrong: Consider a simple maze where the ball needs to roll through position (2, 2) horizontally to reach (2, 4), then later roll through the same position (2, 2) vertically to reach the destination at (4, 2). If we mark (2, 2) as visited during the horizontal roll, the vertical path becomes blocked.
Correct Approach: Only mark positions as visited where the ball actually stops (when it hits a wall):
def dfs(row, col):
if visited[row][col]:
return
visited[row][col] = True # Only mark stopping position
for delta_row, delta_col in directions:
next_row, next_col = row, col
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
# No marking during rolling
dfs(next_row, next_col) # Only explore from stopping position
Pitfall 2: Incorrect Boundary Check in While Loop
The Problem: Using the wrong boundary check can cause index out of bounds errors or incorrect stopping positions.
Incorrect Implementation:
# WRONG: Checking current position instead of next position 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
Why It's Wrong: This checks if the current position is valid and empty, but then moves to the next position without verifying it. The ball could roll into a wall or out of bounds.
Correct Approach: 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
Pitfall 3: Not Handling the Case Where Start Equals Destination
The Problem:
If the starting position is the same as the destination, the solution should return True
only if the ball can move away and come back to stop at that position. Simply returning True
immediately would be incorrect.
Incorrect Implementation:
def hasPath(maze, start, destination):
if start == destination:
return True # WRONG: Assumes ball can stop without moving
# ... rest of the code
Why It's Wrong: The ball might be in the middle of an open area where it cannot stop. It must hit a wall to stop, so even if start equals destination, we need to verify that the ball can actually stop at that position.
Correct Approach: Let the DFS algorithm handle this case naturally. The ball will explore all possible paths, and if it can return to the starting position as a valid stopping point, the algorithm will mark it as visited:
def hasPath(maze, start, destination):
# No special case for start == destination
dfs(start[0], start[1])
return visited[destination[0]][destination[1]]
The algorithm correctly handles this because it only marks positions where the ball can actually stop (after hitting a wall).
You are given an array of intervals where intervals[i] = [start_i, end_i]
represent the start and end of the ith
interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donβt Miss This!