LeetCode 505: The Maze II Solution in Python – A Step-by-Step Guide

Imagine you’re rolling a ball through a maze, where it keeps going until it hits a wall, and you need to find the shortest path from start to finish. That’s the rolling adventure of LeetCode 505: The Maze II, a medium-to-hard problem that’s a fantastic way to practice graph algorithms in Python. We’ll explore two solutions: the Best Solution, using Dijkstra’s algorithm with a priority queue for optimal efficiency, and an Alternative Solution, BFS with distance tracking that’s simpler but less optimized. With detailed examples, clear code, and a friendly tone—especially for the priority queue magic—this guide will help you roll that ball to victory, whether you’re new to coding or leveling up. Let’s navigate the maze and find the shortest path!

What Is LeetCode 505: The Maze II?

In LeetCode 505: The Maze II, you’re given a 2D maze grid (0s for empty spaces, 1s for walls), a start position, and a destination. A ball rolls in one direction (up, down, left, right) until it hits a wall, and you need to find the shortest distance (number of steps) to reach the destination—or return -1 if impossible. Unlike typical maze problems, the ball doesn’t stop at each cell; it rolls until obstructed. For example, in a 5x5 maze, rolling from (0,4) to (4,4) might take 12 steps. This builds on LeetCode 490: The Maze, adding distance tracking.

Problem Statement

  • Input:
    • maze: 2D list of 0s (empty) and 1s (walls).
    • start: [row, col] starting position.
    • destination: [row, col] target position.
  • Output: Integer—shortest distance to destination, or -1 if unreachable.
  • Rules: Ball rolls until hitting a wall; distance is total steps rolled.

Constraints

  • Maze size: 1 ≤ rows, cols ≤ 100.
  • start and destination are within bounds, not walls.
  • At least one empty cell exists.

Examples

  • Input: 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]
    • Output: 12
    • Roll down to (4,4) via stops.
  • Input: 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 = [3,2]
    • Output: -1
    • No path exists.
  • Input: maze = [[0,0],[0,0]], start = [0,0], destination = [1,1]
    • Output: 2
    • Down, then right.

Understanding the Problem: Rolling Through the Maze

To solve LeetCode 505: The Maze II in Python, we need a method to simulate the ball’s rolling, track distances, and find the shortest path in a maze where movement isn’t cell-by-cell but wall-to-wall. A naive approach might try all paths, but with a 100x100 grid, we need efficiency. We’ll use:

  • Best Solution (Dijkstra’s with Priority Queue): O(mn log mn) time, O(mn) space—optimal for shortest path.
  • Alternative Solution (BFS with Distance): O(mn) time, O(mn) space—simpler but explores more.

Let’s dive into Dijkstra’s with a friendly breakdown!

Best Solution: Dijkstra’s Algorithm with Priority Queue

Why Dijkstra’s Is the Best

Dijkstra’s algorithm with a priority queue is the top pick for LeetCode 505 because it guarantees the shortest path in a weighted graph (here, weights are roll distances). Using a min-heap, it’s O(mn log mn) time (m, n are maze dimensions) and O(mn) space—perfect for efficiently exploring stops. It’s like having a GPS that always picks the quickest route through the maze!

How It Works

Think of this as a rolling roadmap:

  • Step 1: Setup:
    • Use a priority queue (min-heap) with (distance, row, col).
    • Track shortest distance to each cell in a distances array.
  • Step 2: Roll in Four Directions:
    • From each stop, roll up, down, left, right until hitting a wall.
    • Calculate distance rolled.
  • Step 3: Explore Greedily:
    • Pop smallest distance from heap.
    • If shorter than known distance to that stop, update and push neighbors.
  • Step 4: Check Destination:
    • Return distance to destination or -1 if unreachable.
  • Why It Works:
    • Priority queue ensures we process shortest paths first.
    • Rolling simulates problem rules.

It’s like rolling with a smart compass!

Step-by-Step Example

Example: 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]

  • Init:
    • Heap: [(0, 0, 4)].
    • distances = { (0,4): 0 }.
  • Step 1: Pop (0, 0, 4):
    • Roll up: Hit (-1,4), stop at (0,4).
    • Roll down: Hit (5,4), stop at (4,4), dist = 4.
      • distances[(4,4)] = 4, push (4, 4, 4).
    • Roll left: Hit (0,2), stop at (0,2), dist = 2.
      • distances[(0,2)] = 2, push (2, 0, 2).
    • Roll right: Hit (0,5), stop at (0,4).
  • Step 2: Pop (2, 0, 2):
    • Roll down: Stop at (2,2), dist = 2 + 2 = 4.
      • distances[(2,2)] = 4, push (4, 2, 2).
    • Roll left: Stop at (0,0), dist = 2.
      • distances[(0,0)] = 2, push (2, 0, 0).
  • Step 3: Pop (2, 0, 0):
    • Roll down: Stop at (4,0), dist = 4.
      • distances[(4,0)] = 6, push (6, 4, 0).
  • Step 4: Pop (4, 2, 2):
    • Roll right: Stop at (2,4), dist = 2.
      • distances[(2,4)] = 6, push (6, 2, 4).
  • Step 5: Pop (4, 4, 4):
    • Destination reached, check distances[(4,4)] = 4.
  • Continue: Explore others, but 4 is shortest to (4,4).
  • Result: 12 (after further optimization, see full path).

Code with Detailed Line-by-Line Explanation

Here’s the Python code, explained for beginners:

import heapq

class Solution:
    def shortestDistance(self, maze: List[List[int]], start: List[int], destination: List[int]) -> int:
        m, n = len(maze), len(maze[0])
        start, dest = tuple(start), tuple(destination)

        # Step 1: Setup priority queue and distances
        pq = [(0, start[0], start[1])]  # (distance, row, col)
        distances = {start: 0}          # Shortest distance to each point

        # Directions: up, right, down, left
        directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]

        # Step 2: Dijkstra’s algorithm
        while pq:
            dist, r, c = heapq.heappop(pq)

            # If reached destination
            if (r, c) == dest:
                return dist

            # If this path isn’t shortest, skip
            if dist > distances[(r, c)]:
                continue

            # Step 3: Roll in all directions
            for dr, dc in directions:
                new_r, new_c = r, c
                steps = 0

                # Roll until hitting a wall
                while 0 <= new_r + dr < m and 0 <= new_c + dc < n and maze[new_r + dr][new_c + dc] == 0:
                    new_r += dr
                    new_c += dc
                    steps += 1

                new_dist = dist + steps
                new_pos = (new_r, new_c)

                # If shorter path found
                if new_pos not in distances or new_dist < distances[new_pos]:
                    distances[new_pos] = new_dist
                    heapq.heappush(pq, (new_dist, new_r, new_c))

        return -1  # Unreachable
  • Lines 6-7: Convert inputs to tuples for hashing.
  • Lines 10-11: Min-heap with initial state; distances dict.
  • Line 14: Four rolling directions.
  • Lines 17-20: Pop shortest distance; return if destination.
  • Line 23: Skip if not shortest path to this stop.
  • Lines 26-36: Roll in each direction until wall, count steps.
  • Lines 39-42: If new shorter path, update and push to heap.
  • Time Complexity: O(mn log mn)—heap ops for mn cells.
  • Space Complexity: O(mn)—heap and distances.

It’s like a rolling pathfinder!

Alternative Solution: BFS with Distance Tracking

Why an Alternative Approach?

BFS with distance tracking explores all stops level-by-level, tracking distances. It’s O(mn) time and O(mn) space—simpler to grasp but less efficient than Dijkstra’s since it doesn’t prioritize shortest paths. Great for understanding maze traversal basics.

How It Works

Picture this as a rolling wave:

  • Step 1: Queue with (row, col, distance).
  • Step 2: Roll in four directions from each stop.
  • Step 3: Track visited stops and distances.
  • Step 4: Return shortest to destination or -1.

It’s like flooding the maze with rolls!

Step-by-Step Example

Example: maze = [[0,0],[0,0]], start = [0,0], destination = [1,1]

  • Init: Queue: [(0,0,0)], distances = {(0,0): 0}.
  • Step 1: Dequeue (0,0,0):
    • Down: (1,0), dist = 1, queue += (1,0,1).
    • Right: (0,1), dist = 1, queue += (0,1,1).
  • Step 2: Dequeue (1,0,1):
    • Right: (1,1), dist = 2, queue += (1,1,2).
  • Step 3: Dequeue (0,1,1):
    • Down: (1,1), dist = 2 (already 2).
  • Step 4: Dequeue (1,1,2):
    • Destination, return 2.
  • Result: 2.

Code for BFS

from collections import deque

class Solution:
    def shortestDistance(self, maze: List[List[int]], start: List[int], destination: List[int]) -> int:
        m, n = len(maze), len(maze[0])
        start, dest = tuple(start), tuple(destination)

        queue = deque([(start[0], start[1], 0)])  # (row, col, distance)
        distances = {start: 0}

        directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]

        while queue:
            r, c, dist = queue.popleft()

            if (r, c) == dest:
                return dist

            for dr, dc in directions:
                new_r, new_c = r, c
                steps = 0

                while 0 <= new_r + dr < m and 0 <= new_c + dc < n and maze[new_r + dr][new_c + dc] == 0:
                    new_r += dr
                    new_c += dc
                    steps += 1

                new_dist = dist + steps
                new_pos = (new_r, new_c)

                if new_pos not in distances or new_dist < distances[new_pos]:
                    distances[new_pos] = new_dist
                    queue.append((new_r, new_c, new_dist))

        return -1
  • Time Complexity: O(mn)—visits each cell once.
  • Space Complexity: O(mn)—queue and distances.

It’s a rolling flood!

Comparing the Two Solutions

  • Dijkstra’s (Best):
    • Pros: O(mn log mn), optimal shortest path.
    • Cons: Heap logic.
  • BFS (Alternative):
    • Pros: O(mn), simpler.
    • Cons: Explores more than needed.

Dijkstra’s wins for efficiency!

Additional Examples and Edge Cases

  • [[0]], [0,0], [0,0]: 0—start = dest.
  • [[0,1],[1,0]], [0,0], [1,1]: -1—blocked.
  • [[0,0,0],[0,1,0],[0,0,0]], [0,0], [2,2]: 4.

Dijkstra’s handles them all!

Complexity Recap

  • Dijkstra’s: Time O(mn log mn), Space O(mn).
  • BFS: Time O(mn), Space O(mn).

Dijkstra’s is the shortest-path king!

Key Takeaways

  • Dijkstra’s: Priority = shortest—learn at Python Basics!
  • BFS: Simple exploration.
  • Rolling: Unique maze twist.
  • Fun: Navigate like a pro!

Final Thoughts: Roll to the Goal!

LeetCode 505: The Maze II in Python is a thrilling maze challenge. Dijkstra’s with a priority queue nails the shortest path, while BFS keeps it basic. Want more? Try LeetCode 490: The Maze or LeetCode 743: Network Delay Time. Ready to roll? Head to Solve LeetCode 505 on LeetCode and find that shortest distance today!