499. The Maze III


Problem Description

In this problem, we are given a m x n maze represented by a 2D grid with 0 indicating empty spaces and 1 indicating walls. A ball can roll in the maze in the cardinal directions (up, down, left, or right), but it only stops when it hits a wall. The ball stops moving further in the same direction once it encounters a wall. Additionally, there is a hole in the maze; if the ball goes over the hole, it will fall into it, and that is the target location the ball needs to reach.

The ball's start position and the hole's position are both given. The goal is to find a sequence of directions (as a string) that will lead the ball into the hole using the shortest path possible. If the shortest path is not unique, the lexicographically smallest sequence of directions should be returned. A sequence is considered lexicographically smaller if it has the smallest alphabetical order possible. If there is no possible way for the ball to fall into the hole, the function should return the string "impossible".

The valid directions the ball can roll are denoted by: 'u' for up, 'd' for down, 'l' for left, and 'r' for right. The distance is measured by the number of empty spaces the ball rolls over from its start to the hole, not counting the starting square but including the final square over the hole.

The maze is framed by walls, so the ball cannot roll beyond the limits of the grid.

Intuition

The intuition behind the given solution is founded on Breadth-first Search (BFS), a common algorithm for exploring paths in a graph or grid-like structures like a maze.

The BFS works by exploring all potential moves from a given position before moving on to the next set of positions. In this context, the valid moves for the ball are to keep rolling in one of the four cardinal directions until it hits a wall.

We use BFS to explore each possible path the ball can take and keep track of the distance travelled and the path taken for each visited position in the maze:

  1. Keep Distance and Path Information: Each position in the maze maintains two pieces of information. One is the minimum number of steps taken to reach that position from the initial ball position. The other is the lexicographically smallest sequence of moves that the ball takes to reach that position.

  2. Exploring the Maze: From the current position, we consider all four possible directions the ball can roll. For each direction, we roll the ball until it hits a wall or falls into the hole.

  3. Tracking Shortest Paths: When the ball stops, we check if the distance to this new stopped position is less than any previously recorded distance. If it is less, or if the distances are equal but the new path is lexicographically smaller, we update the distance and path for that position.

  4. Queue for BFS: We add each new position that we can roll to (except the hole) to a queue to explore in the future. This ensures that all possible moves from each position are considered before moving further away.

  5. Final Check: Once the entire maze has been explored, we check the information for the hole's position. If a path was found, it will contain the lexicographically smallest sequence of directions to get there. If no path was found, the original None assignment will remain, and we will return "impossible" instead.

The provided solution neatly consolidates this approach using a queue data structure to perform the BFS, a 2D array for storing the distance, and another 2D array for storing the path strings for each position the ball stops at.

Learn more about Depth-First Search, Breadth-First Search, Graph, Shortest Path and Heap (Priority Queue) patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece๏ผš

How does merge sort divide the problem into subproblems?

Solution Approach

The implementation uses a BFS algorithm to traverse the maze and search for the shortest path to the hole. Here's the detailed breakdown of the steps and the data structures used:

  1. Initialization: We define the maze's dimensions m and n. The starting position r, c (row, column) of the ball is extracted from the ball list. Similarly, the hole's position rh, ch is extracted from the hole list. We initialize a queue q using deque from the collections module, which will hold the positions to explore. We also define two 2D lists: dist to keep track of the distances and path to keep track of the paths.

  2. Distance and Path Setup: The dist list is filled with inf (infinity) to indicate an unknown distance, while path is filled with None, except for the starting position, which is set to 0 in dist and the empty string '' in path.

  3. BFS Loop: A while loop runs as long as there are positions in the queue q. At each iteration, we dequeue a position and examine all four possible movements from that position. The movements are encoded as tuples (a, b, d), where a and b are the directional increments, and d is the corresponding direction character ('u', 'd', 'l', 'r').

  4. Valid Movement Checking: Inside this loop, we check if moving in a particular direction will stop the ball at a different position. We simulate the ball's movement by incrementally adding the direction increments a and b to the current position x, y until the ball hits a wall or the hole.

  5. Updating Distances and Paths: When the ball cannot move any further in a direction, we check if this new position is either closer than any previously recorded position or if it's the same distance but with a lexicographically smaller path. If either is true, we update dist and path.

  6. Enqueueing Next Positions: If the new stopped position is not the hole, it is added to the queue for further exploration.

  7. Returning the Result: Once the BFS completes, the path at the hole's position path[rh][ch] represents the shortest path to the hole. If no path was found, path[rh][ch] will be None, and we will return 'impossible'.

The algorithm efficiently finds the shortest path by only updating positions when a better path is found. It ensures that the path followed is both the shortest in terms of distance and lexicographically minimal due to the updates in dist and path.

The data structures used โ€” the queue for BFS and 2D arrays for distances and paths โ€” are well-suited for the task. These choices allow the algorithm to queue future positions to visit and maintain necessary state information for each position in the maze.

This breadth-first search ensures that we examine all moves from a starting point before moving outward, thus always finding the shortest path to any reachable position.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".

What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?

Example Walkthrough

Let's consider a small maze, where 0 indicates an empty space, and 1 indicates a wall. We are given the ball's start position and the hole's target position. We will demonstrate the steps of the BFS algorithm to find the shortest path.

For the example, our maze (grid) will look like this:

11 1 1 1
21 0 0 1
31 0 1 1
41 0 0 0
51 1 1 1

Let the ball start position be at (1, 1) and the hole's target position be at (3, 2). Note that we are using (row, column) indexing.

Initial Setup

1Start position (S): (1, 1)
2Hole position (H): (3, 2)
3
4Maze:
5    1 1 1 1
6    1 S 0 1
7    1 0 1 1
8    1 0 H 0
9    1 1 1 1

We initialize the dist with inf (indicating unknown distance) and path with None for each cell except the start (1, 1), which has a distance of 0 and a path of ''. We enqueue the start position into our queue q.

Step-by-Step BFS

  1. We dequeue (1, 1) and start exploring all four directions.

  2. From (1, 1), we can roll down (d) to (3, 1), since it is the next wall or hole position vertically. We update dist[3][1] to 2 and path[3][1] to 'd', and enqueue (3, 1).

  3. Enqueue order: (3, 1).

  4. Dequeue (3, 1) and explore. It can go right (r) to the hole (3, 2). We update dist[3][2] to 3 and path[3][2] to 'dr'. There are no more enqueues because we've reached the hole.

  5. No more positions to explore in the queue; the BFS ends.

Final Check

We consult the final dist and path values at the hole position (3, 2) and find a distance of 3 and a path of 'dr'.

Hence, the shortest path for the ball to fall into the hole is dr (down, then right), with only 3 moves disregarding the starting square.

This example walk-through demonstrates the BFS approach, where we systematically explore all directions from the starting point, updating distances and paths, until we find the shortest lexicographically smallest path to the hole.

Solution Implementation

1from collections import deque
2from math import inf
3
4class Solution:
5    def findShortestWay(self, maze: List[List[int]], ball: List[int], hole: List[int]) -> str:
6        # Dimensions of the maze
7        num_rows, num_cols = len(maze), len(maze[0])
8      
9        # Start position
10        start_row, start_col = ball
11      
12        # Hole position
13        hole_row, hole_col = hole
14      
15        # Queue for the BFS
16        queue = deque([(start_row, start_col)])
17      
18        # Distance matrix initialized with infinity
19        distance = [[inf] * num_cols for _ in range(num_rows)]
20        distance[start_row][start_col] = 0
21      
22        # Path matrix to store the path taken to reach each cell
23        path = [[""] * num_cols for _ in range(num_rows)]
24      
25        # Perform a BFS
26        while queue:
27            current_row, current_col = queue.popleft()
28          
29            # Directions: (row_change, col_change, direction_string)
30            for row_change, col_change, direction in [(-1, 0, 'u'), (1, 0, 'd'), (0, -1, 'l'), (0, 1, 'r')]:
31                x, y = current_row, current_col
32                steps = distance[current_row][current_col]
33              
34                # Keep rolling in the direction until hitting a wall or the hole
35                while (0 <= x + row_change < num_rows
36                       and 0 <= y + col_change < num_cols
37                       and maze[x + row_change][y + col_change] == 0
38                       and (x, y) != (hole_row, hole_col)):
39                    x += row_change
40                    y += col_change
41                    steps += 1
42              
43                # If the distance to the new position is less, or if the distance is the same,
44                # but the path is lexicographically smaller, update the distance and path.
45                if distance[x][y] > steps or \
46                   (distance[x][y] == steps and path[current_row][current_col] + direction < path[x][y]):
47                    distance[x][y] = steps
48                    path[x][y] = path[current_row][current_col] + direction
49                  
50                    # If the new position is not the hole, add it to the queue.
51                    if (x, y) != (hole_row, hole_col):
52                        queue.append((x, y))
53      
54        # If no path was found to the hole, return "impossible".
55        return path[hole_row][hole_col] or 'impossible'
56
1class Solution {
2    public String findShortestWay(int[][] maze, int[] ball, int[] hole) {
3        int rows = maze.length; // Number of rows in the maze
4        int cols = maze[0].length; // Number of columns in the maze
5        int ballRow = ball[0], ballCol = ball[1]; // Starting point (ball)
6        int holeRow = hole[0], holeCol = hole[1]; // Ending point (hole)
7
8        // Queue for BFS search
9        Deque<int[]> queue = new LinkedList<>();
10        queue.offer(new int[]{ballRow, ballCol});
11
12        // Initialize distance array with max values
13        int[][] distance = new int[rows][cols];
14        for (int i = 0; i < rows; ++i) {
15            Arrays.fill(distance[i], Integer.MAX_VALUE);
16        }
17        distance[ballRow][ballCol] = 0; // Starting point distance is 0
18
19        // Path strings for each coordinate
20        String[][] path = new String[rows][cols];
21        path[ballRow][ballCol] = ""; // Empty path for the starting point
22
23        // Directions array with row and column movements (up, down, left, right)
24        int[][] directions = {
25            {-1, 0, 'u'},
26            {1, 0, 'd'},
27            {0, -1, 'l'},
28            {0, 1, 'r'}
29        };
30
31        // BFS search loop
32        while (!queue.isEmpty()) {
33            int[] point = queue.poll();
34            int i = point[0], j = point[1];
35
36            // Iterate through possible directions
37            for (int[] direction : directions) {
38                int vertical = direction[0];
39                int horizontal = direction[1];
40                String directionChar = String.valueOf((char) (direction[2]));
41                int x = i, y = j;
42                int steps = distance[i][j];
43
44                // Roll in the current direction until hitting a wall or the hole
45                while (x + vertical >= 0 && x + vertical < rows && y + horizontal >= 0 && y + horizontal < cols &&
46                        maze[x + vertical][y + horizontal] == 0 && !(x == holeRow && y == holeCol)) {
47                    x += vertical;
48                    y += horizontal;
49                    ++steps;
50                }
51
52                // Check if a shorter path to (x, y) is found or a lexicographically smaller path of the same distance
53                if (distance[x][y] > steps || 
54                   (distance[x][y] == steps && (path[i][j] + directionChar).compareTo(path[x][y]) < 0)) {
55                    distance[x][y] = steps;
56                    path[x][y] = path[i][j] + directionChar;
57                  
58                    // If we have not reached the hole, add the new point to the queue
59                    if (x != holeRow || y != holeCol) {
60                        queue.offer(new int[]{x, y});
61                    }
62                }
63            }
64        }
65
66        // Return the path to the hole or "impossible" if no path is found
67        return path[holeRow][holeCol] == null ? "impossible" : path[holeRow][holeCol];
68    }
69}
70
1#include <vector>
2#include <queue>
3#include <string>
4#include <climits>
5
6using namespace std;
7
8class Solution {
9public:
10    // Method to find the shortest path for the ball to fall into the hole in the maze
11    string findShortestWay(vector<vector<int>>& maze, vector<int>& ball, vector<int>& hole) {
12        int rows = maze.size();       // Number of rows in the maze
13        int cols = maze[0].size();    // Number of columns in the maze
14        int ballRow = ball[0];        // Ball's starting row
15        int ballCol = ball[1];        // Ball's starting column
16        int holeRow = hole[0];        // Hole's row position
17        int holeCol = hole[1];        // Hole's column position
18      
19        // Queue for BFS, storing pairs of (row, column)
20        queue<pair<int, int>> queue;
21        queue.push({ballRow, ballCol});
22      
23        // Distance matrix, initialized to INT_MAX
24        vector<vector<int>> distance(rows, vector<int>(cols, INT_MAX));
25        distance[ballRow][ballCol] = 0;
26      
27        // Path matrix, storing the path to reach each cell
28        vector<vector<string>> path(rows, vector<string>(cols, ""));
29      
30        // Possible directions to move in the maze with corresponding direction characters
31        vector<vector<int>> directions = {{-1, 0, 'u'}, {1, 0, 'd'}, {0, -1, 'l'}, {0, 1, 'r'}};
32      
33        // BFS to find the shortest path
34        while (!queue.empty()) {
35            auto current = queue.front();
36            queue.pop();
37            int currentRow = current.first;
38            int currentCol = current.second;
39          
40            // Explore each direction
41            for (auto& dir : directions) {
42                int dRow = dir[0];
43                int dCol = dir[1];
44                char directionChar = (char)dir[2];
45                int nextRow = currentRow;
46                int nextCol = currentCol;
47                int steps = distance[currentRow][currentCol];
48              
49                // Roll the ball until it hits a wall or the hole
50                while (nextRow + dRow >= 0 && nextRow + dRow < rows &&
51                       nextCol + dCol >= 0 && nextCol + dCol < cols &&
52                       maze[nextRow + dRow][nextCol + dCol] == 0 &&
53                       (nextRow != holeRow || nextCol != holeCol)) {
54                    nextRow += dRow;
55                    nextCol += dCol;
56                    ++steps;
57                }
58              
59                // Update distance and path if a shorter path is found
60                if (distance[nextRow][nextCol] > steps || 
61                    (distance[nextRow][nextCol] == steps && (path[currentRow][currentCol] + directionChar < path[nextRow][nextCol]))) {
62                    distance[nextRow][nextCol] = steps;
63                    path[nextRow][nextCol] = path[currentRow][currentCol] + directionChar;
64                    if (nextRow != holeRow || nextCol != holeCol) {
65                        queue.push({nextRow, nextCol});
66                    }
67                }
68            }
69        }
70      
71        return path[holeRow][holeCol].empty() ? "impossible" : path[holeRow][holeCol];
72    }
73};
74
1// Type definition for a point in the maze represented by row and column
2type Point = { row: number; col: number };
3
4// Function to find the shortest path for the ball to fall into the hole in the maze using BFS
5function findShortestWay(maze: number[][], ball: [number, number], hole: [number, number]): string {
6    const rows = maze.length;       // Number of rows in the maze
7    const cols = maze[0].length;    // Number of columns in the maze
8    const ballPoint: Point = { row: ball[0], col: ball[1] }; // Starting point of the ball
9    const holePoint: Point = { row: hole[0], col: hole[1] }; // Location of the hole
10
11    // Queue for BFS initialized with starting ball point
12    const queue: Point[] = [ballPoint];
13
14    // Distance matrix initialized to Infinity
15    const distance: number[][] = Array.from({ length: rows }, () => Array(cols).fill(Infinity));
16    distance[ballPoint.row][ballPoint.col] = 0;
17
18    // Path matrix, storing the path to reach each cell
19    const path: string[][] = Array.from({ length: rows }, () => Array(cols).fill(""));
20
21    // Possible directions to move in the maze with corresponding direction characters
22    const directions = [{ dr: -1, dc: 0, dcChar: 'u' }, { dr: 1, dc: 0, dcChar: 'd' }, { dr: 0, dc: -1, dcChar: 'l' }, { dr: 0, dc: 1, dcChar: 'r' }];
23
24    // BFS to find the shortest path
25    while (queue.length > 0) {
26        const current = queue.shift()!;
27        const currentRow = current.row;
28        const currentCol = current.col;
29
30        // Explore each direction
31        for (const { dr, dc, dcChar } of directions) {
32            let nextRow = currentRow;
33            let nextCol = currentCol;
34            let steps = distance[currentRow][currentCol];
35
36            // Roll the ball until it hits a wall or the hole
37            while (
38                nextRow + dr >= 0 &&
39                nextRow + dr < rows &&
40                nextCol + dc >= 0 &&
41                nextCol + dc < cols &&
42                maze[nextRow + dr][nextCol + dc] === 0 &&
43                (nextRow !== holePoint.row || nextCol !== holePoint.col)
44            ) {
45                nextRow += dr;
46                nextCol += dc;
47                steps++;
48            }
49
50            // Update distance and path if a shorter path is found
51            if (
52                distance[nextRow][nextCol] > steps ||
53                (
54                    distance[nextRow][nextCol] === steps &&
55                    (path[currentRow][currentCol] + dcChar) < path[nextRow][nextCol]
56                )
57            ) {
58                distance[nextRow][nextCol] = steps;
59                path[nextRow][nextCol] = path[currentRow][currentCol] + dcChar;
60                if (nextRow !== holePoint.row || nextCol !== holePoint.col) {
61                    queue.push({ row: nextRow, col: nextCol });
62                }
63            }
64        }
65    }
66
67    return path[holePoint.row][holePoint.col] || "impossible";
68}
69
70// Variables and function can be called or used from here onwards as global variables
71
Not Sure What to Study? Take the 2-min Quiz๏ผš

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Time and Space Complexity

Time Complexity

The given code implements a breadth-first search (BFS) algorithm on a matrix to find the shortest path for a ball to roll from its starting position to the hole. Each move consists of the ball rolling in a straight line until it hits a wall or falls into the hole.

The time complexity of the BFS algorithm is generally O(V + E), where V is the number of vertices and E is the number of edges in the graph. However, for a two-dimensional matrix, we should consider the following:

  • Each cell in the matrix can be visited multiple times because the ball can reach the same cell from different directions. There are m * n cells, where m is the number of rows and n is the number of columns.
  • For each cell (i, j), the ball can roll in four directions until it hits a wall or a boundary of the maze. Therefore, in the worst-case scenario, it might traverse the entire length or width of the maze.
  • On average, the ball rolls m/2 or n/2 steps before hitting a wall or the boundary in either direction.

Taking these into consideration, the worst-case time complexity can be approximated as:

O((m * n) * (m + n))

This is because, for each of the m * n possible starting points, the ball may have to traverse an entire row or column (m + n steps in the worst case).

Space Complexity

The space complexity of the BFS algorithm depends on the size of the queue used, as well as the additional space for the dist and path arrays:

  • The queue can grow up to m * n in size, as it contains positions representing the cells visited by the ball.
  • The dist and path arrays are both sized m * n.

Hence, the overall space complexity is:

O(m * n + m * n) = O(2 * m * n) = O(m * n)

Overall, both dist and path arrays contribute to the space complexity equally, but constants are dropped in Big O notation, resulting in a total space complexity of O(m * n).

Learn more about how to find time and space complexity quickly using problem constraints.

Fast Track Your Learning with Our Quick Skills Quiz:

Given an array of 1,000,000 integers that is almost sorted, except for 2 pairs of integers. Which algorithm is fastest for sorting the array?


Recommended Readings


Got a question?ย Ask the Teaching Assistantย anything you don't understand.

Still not clear? Ask in the Forum, ย Discordย orย Submitย the part you don't understand to our editors.

โ†
โ†‘TA ๐Ÿ‘จโ€๐Ÿซ