Prerequisites

Before attempting this problem, you should be comfortable with:

  • Graph Traversal (DFS/BFS) - The ball's movement through the maze is modeled as graph exploration
  • Dijkstra's Algorithm - The optimal solution uses Dijkstra's to find the shortest weighted path
  • Priority Queue / Min-Heap - Required for efficient implementation of Dijkstra's algorithm
  • 2D Matrix Navigation - Rolling the ball requires moving in four directions until hitting a wall

Intuition

Unlike Maze I where we just need to determine reachability, here we need to find the shortest distance. The ball rolls until it hits a wall, and each cell it passes counts as one unit of distance.

We can use DFS with distance tracking: maintain a distance matrix where each cell stores the shortest known distance to reach it. When we find a shorter path to a stopping position, update the distance and continue exploring from there.

Algorithm

  1. Initialize a distance matrix with infinity, set the starting position's distance to 0.
  2. Define a recursive dfs function:
    • For each of the four directions:
      • Roll the ball and count the cells traversed until hitting a wall.
      • Calculate the total distance to the stopping position.
      • If this distance is shorter than the previously recorded distance, update it and recursively explore from the new position.
  3. Start dfs from the initial position.
  4. Return the distance at the destination, or -1 if it remains infinity.
class Solution {
    public int shortestDistance(int[][] maze, int[] start, int[] dest) {
        int[][] distance = new int[maze.length][maze[0].length];
        for (int[] row: distance)
            Arrays.fill(row, Integer.MAX_VALUE);
        distance[start[0]][start[1]] = 0;
        dfs(maze, start, distance);
        return distance[dest[0]][dest[1]] == Integer.MAX_VALUE ? -1 : distance[dest[0]][dest[1]];
    }

    public void dfs(int[][] maze, int[] start, int[][] distance) {
        int[][] dirs={{0,1}, {0,-1}, {-1,0}, {1,0}};
        for (int[] dir: dirs) {
            int x = start[0] + dir[0];
            int y = start[1] + dir[1];
            int count = 0;
            while (x >= 0 && y >= 0 && x < maze.length && y < maze[0].length && maze[x][y] == 0) {
                x += dir[0];
                y += dir[1];
                count++;
            }
            if (distance[start[0]][start[1]] + count < distance[x - dir[0]][y - dir[1]]) {
                distance[x - dir[0]][y - dir[1]] = distance[start[0]][start[1]] + count;
                dfs(maze, new int[]{x - dir[0],y - dir[1]}, distance);
            }
        }
    }
}

Time & Space Complexity

  • Time complexity: O(mnmax(m,n))O(m \cdot n \cdot max(m,n))
  • Space complexity: O(mn)O(m \cdot n)

Where mm and nn are the number of rows and columns in maze.


Intuition

BFS can also solve this problem, but unlike typical BFS for shortest path, we cannot stop when we first reach the destination. This is because the ball rolls varying distances, so the first path to reach a position might not be the shortest.

Instead, we use BFS with distance relaxation: whenever we find a shorter path to a stopping position, we update the distance and add it back to the queue for further exploration.

Algorithm

  1. Initialize a distance matrix with infinity and set the start position's distance to 0.
  2. Add the start position to a queue.
  3. While the queue is not empty:
    • Dequeue the current position.
    • For each direction, roll the ball and count the distance.
    • If the new total distance is shorter than the recorded distance at the stopping position, update it and enqueue that position.
  4. Return the distance at the destination, or -1 if unreachable.
class Solution {
    public int shortestDistance(int[][] maze, int[] start, int[] dest) {
        int[][] distance = new int[maze.length][maze[0].length];
        for (int[] row: distance)
            Arrays.fill(row, Integer.MAX_VALUE);

        distance[start[0]][start[1]] = 0;
        int[][] dirs={{0, 1} ,{0, -1}, {-1, 0}, {1, 0}};
        Queue < int[] > queue = new LinkedList < > ();
        queue.add(start);
        while (!queue.isEmpty()) {
            int[] s = queue.remove();
            for (int[] dir: dirs) {
                int x = s[0] + dir[0];
                int y = s[1] + dir[1];
                int count = 0;
                while (x >= 0 && y >= 0 && x < maze.length && y < maze[0].length && maze[x][y] == 0) {
                    x += dir[0];
                    y += dir[1];
                    count++;
                }

                if (distance[s[0]][s[1]] + count < distance[x - dir[0]][y - dir[1]]) {
                    distance[x - dir[0]][y - dir[1]] = distance[s[0]][s[1]] + count;
                    queue.add(new int[] {x - dir[0], y - dir[1]});
                }
            }
        }
        return distance[dest[0]][dest[1]] == Integer.MAX_VALUE ? -1 : distance[dest[0]][dest[1]];
    }
}

Time & Space Complexity

  • Time complexity: O(mn(m+n))O(m \cdot n \cdot (m + n))
  • Space complexity: O(mn)O(m \cdot n)

Where mm and nn are the number of rows and columns in maze.


3. Dijkstra's Algorithm

Intuition

Since edge weights (rolling distances) vary, Dijkstra's algorithm is a natural fit. The key insight is that once we process a position with Dijkstra (after extracting it with the minimum distance), we have found the shortest path to that position.

This version uses a simple implementation where we scan the entire distance matrix to find the unvisited position with minimum distance. While correct, this approach is slower than using a priority queue.

Algorithm

  1. Initialize a distance matrix with infinity and a visited matrix with false. Set start distance to 0.
  2. Repeat until no unvisited positions remain:
    • Find the unvisited position with minimum distance.
    • Mark it as visited.
    • For each direction, roll the ball and count the distance.
    • If the new distance is shorter, update the distance matrix.
  3. Return the distance at the destination, or -1 if unreachable.
class Solution {
    public int shortestDistance(int[][] maze, int[] start, int[] dest) {
        int[][] distance = new int[maze.length][maze[0].length];
        boolean[][] visited = new boolean[maze.length][maze[0].length];
        for (int[] row: distance)
            Arrays.fill(row, Integer.MAX_VALUE);

        distance[start[0]][start[1]] = 0;
        dijkstra(maze, distance, visited);
        return distance[dest[0]][dest[1]] == Integer.MAX_VALUE ? -1 : distance[dest[0]][dest[1]];
    }

    public int[] minDistance(int[][] distance, boolean[][] visited) {
        int[] min={-1,-1};
        int min_val = Integer.MAX_VALUE;

        for (int i = 0; i < distance.length; i++) {
            for (int j = 0; j < distance[0].length; j++) {
                if (!visited[i][j] && distance[i][j] < min_val) {
                    min = new int[] {i, j};
                    min_val = distance[i][j];
                }
            }
        }

        return min;
    }

    public void dijkstra(int[][] maze, int[][] distance, boolean[][] visited) {
        int[][] dirs={{0,1},{0,-1},{-1,0},{1,0}};
        while (true) {
            int[] s = minDistance(distance, visited);
            if (s[0] < 0)
                break;
            visited[s[0]][s[1]] = true;
            for (int[] dir: dirs) {
                int x = s[0] + dir[0];
                int y = s[1] + dir[1];
                int count = 0;
                while (x >= 0 && y >= 0 && x < maze.length && y < maze[0].length && maze[x][y] == 0) {
                    x += dir[0];
                    y += dir[1];
                    count++;
                }

                if (distance[s[0]][s[1]] + count < distance[x - dir[0]][y - dir[1]]) {
                    distance[x - dir[0]][y - dir[1]] = distance[s[0]][s[1]] + count;
                }
            }
        }
    }
}

Time & Space Complexity

  • Time complexity: O((mn)2)O((mn)^2)
  • Space complexity: O(mn)O(mn)

Where mm and nn are the number of rows and columns in maze.


4. Dijkstra's Algorithm and Priority Queue

Intuition

Using a priority queue (min-heap) optimizes Dijkstra's algorithm by efficiently extracting the position with the minimum distance. Instead of scanning the entire matrix, we simply pop from the heap.

When we pop a position from the heap, if its distance is already worse than what we have recorded, we skip it (this handles duplicate entries). Otherwise, we explore all four directions and add newly discovered shorter paths to the heap.

Algorithm

  1. Initialize a distance matrix with infinity. Set start distance to 0.
  2. Add the start position to a min-heap, ordered by distance.
  3. While the heap is not empty:
    • Pop the position with minimum distance.
    • If its distance exceeds the recorded distance, skip it.
    • For each direction, roll the ball and count the distance.
    • If the new distance is shorter, update the distance matrix and push the new position to the heap.
  4. Return the distance at the destination, or -1 if unreachable.
class Solution:
    def shortestDistance(self, maze: List[List[int]], start: List[int], destination: List[int]) -> int:
        distance = [[float('inf')] * len(maze[0]) for _ in range(len(maze))]
        distance[start[0]][start[1]] = 0
        self.dijkstra(maze, start, distance)
        return -1 if distance[destination[0]][destination[1]] == float('inf') else distance[destination[0]][destination[1]]
    
    def dijkstra(self, maze: List[List[int]], start: List[int], distance: List[List[int]]) -> None:
        dirs = [[0, 1], [0, -1], [-1, 0], [1, 0]]
        queue = []
        heapq.heappush(queue, (0, start[0], start[1]))  # (distance, x, y)
        
        while queue:
            dist, sx, sy = heapq.heappop(queue)
            
            if distance[sx][sy] < dist:
                continue
            
            for dx, dy in dirs:
                x, y = sx + dx, sy + dy
                count = 0
                
                while 0 <= x < len(maze) and 0 <= y < len(maze[0]) and maze[x][y] == 0:
                    x += dx
                    y += dy
                    count += 1
                
                if distance[sx][sy] + count < distance[x - dx][y - dy]:
                    distance[x - dx][y - dy] = distance[sx][sy] + count
                    heapq.heappush(queue, (distance[x - dx][y - dy], x - dx, y - dy))

Time & Space Complexity

  • Time complexity: O(mnlog(mn))O(mn \cdot \log(mn))
  • Space complexity: O(mn)O(m \cdot n)

Where mm and nn are the number of rows and columns in maze.


Common Pitfalls

Stopping BFS on First Arrival at Destination

Unlike standard BFS where the first path to a node is the shortest, the ball can reach the same position via paths of different lengths due to variable rolling distances. You must continue exploring until all shorter paths are exhausted. The correct approach is to only finalize a distance when extracted from a priority queue or when no shorter path exists.

Counting Distance Incorrectly During Rolling

The ball rolls through multiple cells before stopping, and each cell traversed counts as one unit of distance. A common error is counting only the stopping position or miscounting the cells rolled. Increment the counter inside the while loop as the ball moves, not after it stops.

Forgetting to Step Back After Hitting a Wall

The rolling loop continues until the ball hits a wall or boundary, meaning the final position is invalid. You must subtract the direction vector once to get the actual stopping position. Forgetting this step causes the ball to be placed inside walls or outside the maze.