Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

499. The Maze III - Leetcode Solution

Problem Description

The Maze III is a pathfinding problem where you are given a 2D grid representing a maze, a ball starting position, and a hole (target) position. The maze consists of empty spaces (represented by 0) and walls (represented by 1). The ball can roll in four directions: up, down, left, or right. However, once the ball starts rolling in a direction, it continues until it hits a wall or falls into the hole. The ball cannot stop before hitting a wall or the hole.

The goal is to find the shortest path for the ball to fall into the hole, and if there are multiple paths of the same shortest length, return the lexicographically smallest path in terms of a string made of 'u', 'd', 'l', and 'r' for up, down, left, and right, respectively. If the ball cannot reach the hole, return "impossible".

  • There is only one valid solution or none.
  • The ball cannot be reused or reset after falling into the hole.
  • Inputs are: maze (2D list), ball (start position), hole (target position).

Thought Process

Initially, it might seem like a simple shortest-path problem, similar to Breadth-First Search (BFS) in a grid. However, the twist is that the ball keeps rolling until it hits a wall or the hole, and cannot stop arbitrarily. This means we must simulate the rolling motion for each direction, not just move one cell at a time.

A brute-force approach would be to try every possible path, but this quickly becomes infeasible due to the exponential number of possible paths and the rolling constraint. Instead, we need an optimized way to find the shortest path while also keeping track of the lexicographically smallest path in case of ties.

The problem is well-suited for a variant of Dijkstra's algorithm, where we track both the minimum distance and the path string for each position. We use a priority queue to always expand the path with the smallest distance and, for ties, the smallest lexicographical path.

Solution Approach

To solve this problem efficiently, we use Dijkstra's algorithm with a priority queue. This ensures we always process the most promising state first and find the shortest and lexicographically smallest path.

  1. State Representation: Each state in the queue consists of: (distance so far, path string, current row, current column).
  2. Priority Queue: The queue is ordered first by distance, then by the path string (to ensure lexicographical order).
  3. Rolling Simulation: For each move, we roll the ball in one of the four directions until it hits a wall or the hole. We keep track of the number of steps taken.
  4. Visited Tracking: For each position, we store the shortest distance and the smallest path string found so far. We only revisit a position if we find a shorter path or a lexicographically smaller path with the same distance.
  5. Early Exit: As soon as the ball falls into the hole, we return the current path string.
  6. Impossible Case: If the queue is exhausted and we haven't reached the hole, we return "impossible".

We use a hash map (or 2D array) to efficiently look up the best (distance, path) for each cell. This keeps the algorithm efficient and avoids redundant processing.

Example Walkthrough

Let's consider the following 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]
    ]
    ball = [0, 4]
    hole = [4, 4]
  
  1. The ball starts at (0, 4). We add this state to the queue: (distance=0, path="", row=0, col=4).
  2. We roll in all four directions. For example, rolling down, the ball moves to (4, 4) in 4 steps and falls into the hole.
  3. Since the ball reaches the hole, we immediately return the path string, which in this case is "d" (down).
  4. If there were multiple ways to reach (4, 4) in 4 steps, we would pick the lexicographically smallest path.

The process ensures we always find the shortest and lexicographically smallest path.

Time and Space Complexity

Brute-force Approach: The brute-force approach would try all possible paths, leading to exponential time complexity: O(4n*m), where n and m are the maze dimensions.

Optimized (Dijkstra's) Approach:

  • Time Complexity: O(n*m*log(n*m)), where n and m are the number of rows and columns. Each cell can be visited at most 4 times (one per direction), and each operation on the priority queue takes log(n*m) time.
  • Space Complexity: O(n*m), for storing the best distance and path for each cell.
This is much more efficient and scalable for larger mazes.

Summary

The Maze III problem requires simulating the rolling behavior of a ball in a maze and finding the shortest path to a hole, with a tie-breaker for lexicographically smallest paths. By modeling the problem as a graph and using Dijkstra's algorithm with a priority queue, we efficiently find the optimal solution. The key insight is to always roll until hitting a wall or the hole, and to track both distance and path string to ensure correctness and optimality.

Code Implementation

import heapq

class Solution:
    def findShortestWay(self, maze, ball, hole):
        m, n = len(maze), len(maze[0])
        dirs = [(-1,0,'u'), (1,0,'d'), (0,-1,'l'), (0,1,'r')]
        heap = [(0, "", ball[0], ball[1])]
        visited = dict()

        while heap:
            dist, path, x, y = heapq.heappop(heap)
            if (x, y) == (hole[0], hole[1]):
                return path
            if (x, y) in visited and (visited[(x, y)][0] < dist or (visited[(x, y)][0]==dist and visited[(x, y)][1] <= path)):
                continue
            visited[(x, y)] = (dist, path)
            for dx, dy, dchar in dirs:
                nx, ny, steps = x, y, 0
                # Roll until hit wall or hole
                while 0 <= nx+dx < m and 0 <= ny+dy < n and maze[nx+dx][ny+dy] == 0:
                    nx += dx
                    ny += dy
                    steps += 1
                    if [nx, ny] == hole:
                        break
                if (nx, ny) not in visited or dist+steps < visited[(nx, ny)][0] or (dist+steps == visited[(nx, ny)][0] and path+dchar < visited[(nx, ny)][1]):
                    heapq.heappush(heap, (dist+steps, path+dchar, nx, ny))
        return "impossible"
      
#include <vector>
#include <string>
#include <queue>
#include <tuple>
#include <unordered_map>
using namespace std;

class Solution {
public:
    string findShortestWay(vector<vector<int>>& maze, vector<int>& ball, vector<int>& hole) {
        int m = maze.size(), n = maze[0].size();
        vector<vector<pair<int, string>>> visited(m, vector<pair<int, string>>(n, {INT_MAX, ""}));
        vector<tuple<int, int, int, string>> dirs = {{-1,0, 'u'}, {1,0, 'd'}, {0,-1, 'l'}, {0,1, 'r'}};
        priority_queue<tuple<int, string, int, int>, vector<tuple<int, string, int, int>>, greater<>> pq;
        pq.emplace(0, "", ball[0], ball[1]);
        
        while (!pq.empty()) {
            auto [dist, path, x, y] = pq.top(); pq.pop();
            if (x == hole[0] && y == hole[1]) return path;
            if (visited[x][y].first < dist || (visited[x][y].first == dist && visited[x][y].second <= path)) continue;
            visited[x][y] = {dist, path};
            for (auto& [dx, dy, dchar] : dirs) {
                int nx = x, ny = y, steps = 0;
                while (nx+dx >= 0 && nx+dx < m && ny+dy >= 0 && ny+dy < n && maze[nx+dx][ny+dy] == 0) {
                    nx += dx; ny += dy; steps++;
                    if (nx == hole[0] && ny == hole[1]) break;
                }
                if (visited[nx][ny].first > dist+steps || (visited[nx][ny].first == dist+steps && path + (char)dchar < visited[nx][ny].second)) {
                    pq.emplace(dist+steps, path + (char)dchar, nx, ny);
                }
            }
        }
        return "impossible";
    }
};
      
import java.util.*;

class Solution {
    public String findShortestWay(int[][] maze, int[] ball, int[] hole) {
        int m = maze.length, n = maze[0].length;
        String[][] visited = new String[m][n];
        int[][] dist = new int[m][n];
        for (int[] d : dist) Arrays.fill(d, Integer.MAX_VALUE);
        PriorityQueue<State> pq = new PriorityQueue<>();
        pq.offer(new State(ball[0], ball[1], 0, ""));
        int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
        char[] dchar = {'u','d','l','r'};
        while (!pq.isEmpty()) {
            State cur = pq.poll();
            int x = cur.x, y = cur.y;
            if (x == hole[0] && y == hole[1]) return cur.path;
            if (dist[x][y] < cur.dist || (dist[x][y] == cur.dist && visited[x][y] != null && visited[x][y].compareTo(cur.path) <= 0)) continue;
            dist[x][y] = cur.dist;
            visited[x][y] = cur.path;
            for (int d = 0; d < 4; d++) {
                int nx = x, ny = y, steps = 0;
                while (nx+dirs[d][0] >= 0 && nx+dirs[d][0] < m && ny+dirs[d][1] >= 0 && ny+dirs[d][1] < n && maze[nx+dirs[d][0]][ny+dirs[d][1]] == 0) {
                    nx += dirs[d][0];
                    ny += dirs[d][1];
                    steps++;
                    if (nx == hole[0] && ny == hole[1]) break;
                }
                if (dist[nx][ny] > cur.dist+steps || (dist[nx][ny] == cur.dist+steps && (visited[nx][ny]==null || cur.path+dchar[d].compareTo(visited[nx][ny]) < 0))) {
                    pq.offer(new State(nx, ny, cur.dist+steps, cur.path + dchar[d]));
                }
            }
        }
        return "impossible";
    }
    class State implements Comparable<State> {
        int x, y, dist;
        String path;
        State(int x, int y, int dist, String path) {
            this.x = x; this.y = y; this.dist = dist; this.path = path;
        }
        public int compareTo(State other) {
            if (this.dist != other.dist) return this.dist - other.dist;
            return this.path.compareTo(other.path);
        }
    }
}
      
class PriorityQueue {
    constructor() { this.data = []; }
    enqueue(item) {
        this.data.push(item);
        this.data.sort((a, b) => a[0] - b[0] || a[1].localeCompare(b[1]));
    }
    dequeue() { return this.data.shift(); }
    isEmpty() { return this.data.length === 0; }
}

var findShortestWay = function(maze, ball, hole) {
    let m = maze.length, n = maze[0].length;
    let dirs = [[-1,0,'u'], [1,0,'d'], [0,-1,'l'], [0,1,'r']];
    let visited = Array.from({length: m}, () => Array(n).fill(null));
    let dist = Array.from({length: m}, () => Array(n).fill(Infinity));
    let pq = new PriorityQueue();
    pq.enqueue([0, "", ball[0], ball[1]]);
    while (!pq.isEmpty()) {
        let [d, path, x, y] = pq.dequeue();
        if (x === hole[0] && y === hole[1]) return path;
        if (dist[x][y] < d || (dist[x][y] === d && visited[x][y] !== null && visited[x][y] <= path)) continue;
        dist[x][y] = d;
        visited[x][y] = path;
        for (let [dx, dy, dchar] of dirs) {
            let nx = x, ny = y, steps = 0;
            while (nx+dx >= 0 && nx+dx < m && ny+dy >= 0 && ny+dy < n && maze[nx+dx][ny+dy] === 0) {
                nx += dx;
                ny += dy;
                steps++;
                if (nx === hole[0] && ny === hole[1]) break;
            }
            if (dist[nx][ny] > d+steps || (dist[nx][ny] === d+steps && (visited[nx][ny] === null || path+dchar < visited[nx][ny]))) {
                pq.enqueue([d+steps, path+dchar, nx, ny]);
            }
        }
    }
    return "impossible";
};