490. The Maze
Problem Description
In this problem, we're given a 2D array that represents a maze, where 0
s represent empty spaces and 1
s represent walls. The maze is surrounded by walls on all borders.
A ball can roll through the maze, but it only stops moving when it hits a wall. Once it's stopped, it can then be rolled again in any of the four cardinal directions (up, down, left, right). The goal is to determine if there is a way to move the ball from its start position to a specified destination by only rolling it in such a way that it stops at the destination. The start position and destination are given as [row, column] coordinates.
The problem asks us to return true
if the ball can reach the destination and stop there, and 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 cell is a node and edges exist between adjacent passable cells (spaces where the ball can roll).
Is it a tree?
- No: The structure of the maze does not guarantee a hierarchical, tree-like structure; multiple paths may exist between points.
Is the problem related to directed acyclic graphs (DAGs)?
- No: This problem is not about directed acyclic graphs; it's more related to exploring all possible paths to find a solution.
Is the problem related to shortest paths?
- No: The goal is to determine if the ball can stop at the destination, not to find the shortest path to it.
Does the problem involve connectivity?
- Yes: We need to find out if there’s a route connecting the start and the destination point in the maze.
Is the graph weighted?
- No: The maze is unweighted; each step from one cell to another is uniform without varying costs.
Conclusion: The flowchart suggests using DFS or BFS for this unweighted connectivity problem. Given the need to explore potentially feasible paths deeply until a dead end or the destination is reached, Depth-First Search (DFS) is particularly suitable for the problem.
Intuition
The solution to this problem involves exploring the maze to find if there is a path to the destination. Exploring a maze is a classic problem that is usually solved using graph traversal algorithms like Depth-First Search (DFS) or Breadth-First Search (BFS). The intuition behind using BFS in this particular problem is that we want to explore all possible paths where the ball can roll until it hits a wall.
The main idea is to simulate the rolling of the ball. We start at the initial position and roll the ball in all possible directions until it hits a wall. Once it comes to a stop, we check if the current stopping point is the destination. If it's not, we treat this stopping point as a new start point and continue to roll the ball in all four directions. We repeat this process until we either reach the destination or all possible paths have been explored without reaching it.
We need to keep track of the positions we have already visited to prevent infinite loops. This is because without tracking, we might keep revisiting the same positions over and over again. For this purpose, we use a set to store visited positions.
By using BFS with a queue and a visited set, we can systematically explore the maze and check if a path to the destination exists.
Learn more about Depth-First Search and Breadth-First Search patterns.
Solution Approach
The implementation of the aforementioned BFS approach is straightforward. To explain how the solution works, let's break it down step by step:
-
Queue Initialization: We initialize a queue
q
for the BFS with the starting position of the ball. This queue will be used to store the positions from where we need to continue exploring further. -
Visited Set: To keep track of the positions we have visited so far to avoid re-processing, we initialize a set
vis
. -
BFS Loop: We enter a loop to process each element in the queue until it's empty, which means we've exhausted all possible movements. For each position taken from the queue: a. We calculate potential new positions by moving the ball in all four possible directions until it hits a wall. b. For each of the four directions, we ensure that the movement is within the bounds of the maze and we are moving into empty space (
0
). c. As soon as the ball stops due to hitting a wall, we check if the stopped position is the destination. If it is, we've found a path, and we returntrue
. d. If the new stopped position is not the destination and hasn't been visited yet, we add it to the visited set and the queue for further exploration. -
Directions Representation: The directions in which the ball can move are represented by the vectors
[[0, -1], [0, 1], [-1, 0], [1, 0]]
, which correspond to left, right, up, and down movements, respectively. -
Exploring Paths: For each position discovered, the loop continues exploring further by seeing where the ball can go next from these new positions, until it either reaches the destination or there are no more positions to explore (the queue becomes empty).
-
Return False: If the queue is exhausted without finding the destination, the function returns
false
, signifying that there is no path that leads to the destination.
The choice of BFS in this case is strategic, as it allows us to explore the maze level by level, considering the shortest paths first and ensuring that once we reach the destination, we've found the optimal solution (should one exist).
Here's the implementation as provided in the reference document:
class Solution:
def hasPath(
self, maze: List[List[int]], start: List[int], destination: List[int]
) -> bool:
m, n = len(maze), len(maze[0])
q = deque([start])
rs, cs = start
vis = {(rs, cs)}
while q:
i, j = q.popleft()
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
if [x, y] == destination:
return True
if (x, y) not in vis:
vis.add((x, y))
q.append((x, y))
return False
This code clearly follows the BFS paradigm, and it allows us to see a practical implementation of the intuition discussed earlier.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's take an example maze and walk through the solution using the BFS approach detailed in the content above.
Suppose we have the following 2D maze where S
is the start position, D
is the destination, 0
represents open space, and 1
represents a wall:
1 1 1 1 1 1 0 0 0 1 1 0 1 0 1 1 S 1 D 1 1 1 1 1 1
The start position is marked as [3, 1]
and the destination is [3, 3]
. We want to find if there's a path for the ball to roll from S
to D
.
-
Queue Initialization: Initialize the queue with the start position
(3, 1)
. -
Visited Set: Initialize the visited set and add the start position to it.
-
BFS Loop: Now, we start processing the position
(3, 1)
and we need to explore all four directions. We'll simulate the rolling of the ball until it hits a wall:- Rolling left is not an option since
(3, 0)
is a wall. - Rolling right ends up at
(3, 2)
, just before the wall. - Rolling up isn't possible due to hitting a wall immediately.
- Rolling down hits a wall immediately.
- Rolling left is not an option since
-
We then mark
(3, 2)
as visited and add it to the queue if it's not already visited. In this case, it's a new position so we add it. -
The queue now has
(3, 2)
, and we pick this and repeat the process. Rolling right from(3, 2)
makes the ball stop at(3, 3)
, which is our destinationD
. -
Once we hit the destination, we return
True
, indicating that there is indeed a path fromS
toD
.
If we continue the loop without finding the destination, we will eventually run out of positions to check, which would compel us to return False
, indicating there's no such path. However, in this example, since we did reach the destination, we successfully exit the loop with the correct outcome.
Solution Implementation
1from collections import deque
2from typing import List
3
4class Solution:
5 def hasPath(self, maze: List[List[int]], start: List[int], destination: List[int]) -> bool:
6 # Get the dimensions of the maze.
7 rows, cols = len(maze), len(maze[0])
8
9 # Initialize a queue to perform BFS and add the starting point.
10 queue = deque([start])
11
12 # Set to keep track of visited positions to avoid loops.
13 visited = set()
14 visited.add(tuple(start))
15
16 # Perform BFS from the start position.
17 while queue:
18 current_position = queue.popleft()
19 row, col = current_position
20
21 # Explore all possible directions: left, right, up, down.
22 for direction_row, direction_col in [[0, -1], [0, 1], [-1, 0], [1, 0]]:
23 new_row, new_col = row, col
24
25 # Move in the current direction as far as possible until hitting a wall.
26 while 0 <= new_row + direction_row < rows and 0 <= new_col + direction_col < cols \
27 and maze[new_row + direction_row][new_col + direction_col] == 0:
28 new_row += direction_row
29 new_col += direction_col
30
31 # If the destination is reached, return True.
32 if [new_row, new_col] == destination:
33 return True
34
35 # If the new position has not been visited, mark as visited and add to queue.
36 if (new_row, new_col) not in visited:
37 visited.add((new_row, new_col))
38 queue.append((new_row, new_col))
39
40 # If no path is found after exploring all possibilities, return False.
41 return False
42
1class Solution {
2 public boolean hasPath(int[][] maze, int[] start, int[] destination) {
3 // Dimensions of the maze
4 int numRows = maze.length;
5 int numCols = maze[0].length;
6
7 // Visited positions marker
8 boolean[][] visited = new boolean[numRows][numCols];
9
10 // Mark the start position as visited
11 visited[start[0]][start[1]] = true;
12
13 // Initialize a queue for BFS
14 Deque<int[]> queue = new LinkedList<>();
15 queue.offer(start);
16
17 // Direction vectors: up, right, down, left
18 int[] directions = {-1, 0, 1, 0, -1};
19
20 // Perform BFS to find the path
21 while (!queue.isEmpty()) {
22 int[] position = queue.poll();
23 int currentRow = position[0], currentCol = position[1];
24
25 // Traverse in all four directions
26 for (int k = 0; k < 4; ++k) {
27 int nextRow = currentRow, nextCol = currentCol;
28 // Direction modifiers for both row and column
29 int rowInc = directions[k], colInc = directions[k + 1];
30
31 // Roll the ball until a wall is hit
32 while (
33 nextRow + rowInc >= 0 && nextRow + rowInc < numRows &&
34 nextCol + colInc >= 0 && nextCol + colInc < numCols &&
35 maze[nextRow + rowInc][nextCol + colInc] == 0
36 ) {
37 nextRow += rowInc;
38 nextCol += colInc;
39 }
40
41 // If the destination is found, return true
42 if (nextRow == destination[0] && nextCol == destination[1]) {
43 return true;
44 }
45
46 // If the new position is not visited, mark it visited and add to queue
47 if (!visited[nextRow][nextCol]) {
48 visited[nextRow][nextCol] = true;
49 queue.offer(new int[] {nextRow, nextCol});
50 }
51 }
52 }
53
54 // Return false if the destination is not reachable
55 return false;
56 }
57}
58
1class Solution {
2public:
3 // Function to check if there is a path in the maze from start to destination.
4 bool hasPath(vector<vector<int>>& maze, vector<int>& start, vector<int>& destination) {
5 int rows = maze.size(); // Number of rows in the maze.
6 int cols = maze[0].size(); // Number of columns in the maze.
7
8 // Queue to perform BFS with the initial position being the start.
9 queue<vector<int>> queue;
10 queue.push(start);
11
12 // Visited array to keep track of visited nodes.
13 vector<vector<bool>> visited(rows, vector<bool>(cols, false));
14 visited[start[0]][start[1]] = true;
15
16 // 4 possible directions to move: up, right, down, left.
17 vector<int> directions = {-1, 0, 1, 0, -1};
18
19 // Start BFS.
20 while (!queue.empty()) {
21 vector<int> position = queue.front();
22 queue.pop();
23 int currentRow = position[0], currentCol = position[1];
24
25 // Explore all possible 4 directions.
26 for (int k = 0; k < 4; ++k) {
27 int newRow = currentRow, newCol = currentCol;
28 int rowDir = directions[k], colDir = directions[k + 1];
29
30 // Roll the ball until it hits a wall.
31 while (newRow + rowDir >= 0 && newRow + rowDir < rows &&
32 newCol + colDir >= 0 && newCol + colDir < cols &&
33 maze[newRow + rowDir][newCol + colDir] == 0) {
34 newRow += rowDir;
35 newCol += colDir;
36 }
37
38 // Check if the new position is the destination.
39 if (newRow == destination[0] && newCol == destination[1]) return true;
40
41 // If the new position has not been visited, mark as visited and enqueue it.
42 if (!visited[newRow][newCol]) {
43 visited[newRow][newCol] = true;
44 queue.push({newRow, newCol});
45 }
46 }
47 }
48
49 // Return false if destination is not reachable.
50 return false;
51 }
52};
53
1// Represents the maze as a 2D array of numbers, 1 for walls and 0 for paths.
2type Maze = number[][];
3
4// Represents a position in the maze with format [row, column].
5type Position = [number, number];
6
7// Function signature to check if there is a path in the maze from start to destination.
8function hasPath(maze: Maze, start: Position, destination: Position): boolean {
9 const rows = maze.length; // Number of rows in the maze.
10 const cols = maze[0].length; // Number of columns in the maze.
11
12 // Queue to perform BFS, initialized with the starting position.
13 const queue: Position[] = [start];
14
15 // Visited matrix to keep track of visited positions.
16 const visited: boolean[][] = Array.from({ length: rows }, () => Array(cols).fill(false));
17 visited[start[0]][start[1]] = true;
18
19 // Possible directions to move: left, down, right, and up.
20 const directions: number[] = [-1, 0, 1, 0, -1];
21
22 // Start BFS exploration.
23 while (queue.length > 0) {
24 const [currentRow, currentCol] = queue.shift()!; // Assume queue is not empty and position is defined.
25
26 // Explore in all four directions.
27 for (let k = 0; k < 4; ++k) {
28 let newRow = currentRow, newCol = currentCol;
29 const rowDir = directions[k], colDir = directions[k + 1];
30
31 // Roll the ball until it hits a wall or the edge of the maze.
32 while (
33 newRow + rowDir >= 0 && newRow + rowDir < rows &&
34 newCol + colDir >= 0 && newCol + colDir < cols &&
35 maze[newRow + rowDir][newCol + colDir] === 0
36 ) {
37 newRow += rowDir;
38 newCol += colDir;
39 }
40
41 // If the new position is the destination, return true.
42 if (newRow === destination[0] && newCol === destination[1]) {
43 return true;
44 }
45
46 // If the new position has not been visited, mark as visited and enqueue it.
47 if (!visited[newRow][newCol]) {
48 visited[newRow][newCol] = true;
49 queue.push([newRow, newCol]);
50 }
51 }
52 }
53
54 // If the destination was not reached, return false.
55 return false;
56}
57
Time and Space Complexity
The given code is an implementation of the Breadth-First Search (BFS) algorithm, which is used to determine whether there is a path from the start to the destination in a given maze using possible movements up, down, left, or right. It stops movement in one direction when it hits a wall or the edge of the maze.
Time Complexity
The time complexity of this function is O(m * n)
, where m
is the number of rows, and n
is the number of columns in the maze. This is because, in the worst case, the algorithm may explore each cell in the maze once. Each cell is added to the queue only once, and each roll in one of four directions continues until it hits a wall, but the rolling action does not increase the total number of cells visited. Each possible space is processed once, so at most, the algorithm will perform m * n
operations.
Space Complexity
The space complexity of the function is also O(m * n)
. This is due to the storage required for the visited set vis
, which in the worst case may contain every cell in the maze. Moreover, the queue q
may also store a significant number of positions although not more than m * n
. However since we only store each cell at most once, the space complexity remains O(m * n)
overall.
Learn more about how to find time and space complexity quickly using problem constraints.
What's the output of running the following function using the following tree as input?
1def serialize(root):
2 res = []
3 def dfs(root):
4 if not root:
5 res.append('x')
6 return
7 res.append(root.val)
8 dfs(root.left)
9 dfs(root.right)
10 dfs(root)
11 return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4 StringJoiner res = new StringJoiner(" ");
5 serializeDFS(root, res);
6 return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10 if (root == null) {
11 result.add("x");
12 return;
13 }
14 result.add(Integer.toString(root.val));
15 serializeDFS(root.left, result);
16 serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2 let res = [];
3 serialize_dfs(root, res);
4 return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8 if (!root) {
9 res.push("x");
10 return;
11 }
12 res.push(root.val);
13 serialize_dfs(root.left, res);
14 serialize_dfs(root.right, res);
15}
16
Recommended Readings
https algomonster s3 us east 2 amazonaws com 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
https algomonster s3 us east 2 amazonaws com 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
LeetCode 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 we
Want a Structured Path to Master System Design Too? Don’t Miss This!