490. The Maze

Problem Description

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

  • 0 represents an empty space
  • 1 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.

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

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:

  1. We need to explore all possible paths from the starting position
  2. From each stopping position, we branch out to explore all 4 directions
  3. 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 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

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)
  • 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]] is True, 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 Evaluator

Example 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.

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 uses O(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).

Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does merge sort divide the problem into subproblems?


Recommended Readings

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

Load More