Taro Logo

The Maze II

Medium
Google logo
Google
4 views
Topics:
GraphsDynamic Programming

You are given a maze represented by a 2D array of 0s and 1s, where 0 is an open path and 1 is a wall. You are also given a start position (row, col) and a destination position (row, col). The ball can roll in four directions: up, down, left, or right. It will continue rolling in a chosen direction until it hits a wall. When the ball stops, it can choose another direction.

The goal is to find the shortest distance for the ball to travel from the start position to the destination position. If there is no such path, return -1.

Example:

maze = [
  [0,0,0,0,0],
  [1,1,0,0,1],
  [0,0,0,1,0],
  [1,1,1,0,1],
  [0,0,0,0,0]
]
start = [0,4]
destination = [4,4]

In the maze above, the shortest distance from start = [0, 4] to destination = [4, 4] is 12. One possible shortest path is:

  1. Right -> Wall (distance = 0)
  2. Down -> Wall (distance = 4)
  3. Left -> Wall (distance = 4)
  4. Down -> Wall (distance = 4)

Return 12.

Now, consider this example:

maze = [
  [0,0,0,0,0],
  [1,1,0,0,1],
  [0,0,0,1,0],
  [1,1,1,0,1],
  [0,0,0,0,0]
]
start = [0,4]
destination = [3,2]

What is the shortest path between start and destination?

Write a function that takes the maze, start position, and destination position as input and returns the shortest distance. If no path exists, return -1.

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. What are the dimensions of the maze, and what is the maximum size it can be?
  2. What values represent walls and empty spaces in the maze? Is it guaranteed to be 0 and 1, or could other values be present?
  3. If the ball cannot reach the destination, what should I return? Should I return -1, or throw an exception?
  4. Is the starting position guaranteed to be an empty cell within the maze, and is the destination guaranteed to be distinct from the starting position?
  5. Are there any constraints on the possible start and destination positions? Could they be at the edge of the maze?

Brute Force Solution

Approach

The brute force approach explores every possible path through the maze, one at a time, until we find the shortest path to the destination. We essentially simulate rolling the ball in every possible direction at each step.

Here's how the algorithm would work step-by-step:

  1. Start at the ball's starting position.
  2. From that position, imagine the ball can roll north, south, east, or west.
  3. For each of those directions, let the ball roll until it hits a wall or the edge of the maze.
  4. Record how far the ball rolled in each of those directions, representing the distance traveled.
  5. Now, for each new position the ball landed on, repeat the process: imagine rolling north, south, east, and west from that position until hitting a wall.
  6. Continue this process, exploring all possible roll sequences, and keeping track of the total distance for each path taken.
  7. If, during this exploration, the ball lands on the destination, compare the total distance traveled to the shortest distance found so far. If the new path is shorter, update the shortest distance.
  8. Keep exploring every possible path, even if it seems long or winding, until all reachable positions have been considered.
  9. After exploring all possible paths, the shortest distance recorded will be the solution.

Code Implementation

def the_maze_two_brute_force(
    maze,
    start_row,
    start_column,
    destination_row,
    destination_column,
):
    number_of_rows = len(maze)
    number_of_columns = len(maze[0])
    shortest_distance = float('inf')

    def depth_first_search(
        current_row,
        current_column,
        current_distance,
        visited,
    ):
        nonlocal shortest_distance

        if (
            current_row == destination_row
            and current_column == destination_column
        ):
            # Found the destination, update shortest if needed.
            shortest_distance = min(
                shortest_distance, current_distance
            )
            return

        visited.add((current_row, current_column))

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

        for delta_row, delta_column in directions:
            row = current_row
            column = current_column
            distance = 0

            # Keep rolling until hitting a wall
            while (
                0 <= row + delta_row < number_of_rows
                and 0 <= column + delta_column < number_of_columns
                and maze[row + delta_row][column + delta_column] == 0
            ):
                row += delta_row
                column += delta_column
                distance += 1

            if distance > 0 and (row, column) not in visited:
                # Explore further from the new position

                depth_first_search(
                    row,
                    column,
                    current_distance + distance,
                    visited.copy(),
                )

    depth_first_search(
        start_row,
        start_column,
        0,
        set(),
    )

    if shortest_distance == float('inf'):
        return -1
    else:
        return shortest_distance

Big(O) Analysis

Time Complexity
O(4^(m*n))The brute force approach explores all possible paths in the maze. In the worst case, from each cell, we can move in four directions (north, south, east, west), until we hit a wall. Consider a maze of size m x n. In the worst-case scenario, we might explore almost every possible path from the start. Since, at each step, the number of choices is four, and the path length can be proportional to m*n, we get an exponential growth of 4 to the power of (m*n) possible paths. Therefore, the overall time complexity is O(4^(m*n)).
Space Complexity
O(N*M)The brute force approach implicitly uses a queue (or stack, depending on the implementation of the exploration) to keep track of the positions to visit and the distances to those positions. In the worst-case scenario, the queue might contain every cell in the maze if all cells are reachable from the starting position, where N is the number of rows and M is the number of columns. Additionally, we might use a separate data structure to keep track of the shortest distance to each cell, which can also have a size of N*M. Therefore, the space complexity is O(N*M), where N*M represents the dimensions of the maze.

Optimal Solution

Approach

Imagine finding the shortest path through a maze using the fewest steps. The key is to explore the maze methodically, always remembering the shortest distances we've already found to each spot. We use this information to avoid dead ends and quickly discover the most efficient route.

Here's how the algorithm would work step-by-step:

  1. Start at the ball's initial location.
  2. Imagine rolling the ball in each of the four directions (up, down, left, right) until it hits a wall.
  3. For each direction, calculate the distance the ball traveled before hitting the wall.
  4. Keep track of the shortest distance found so far to reach each location in the maze.
  5. If a new path to a location is shorter than the previously recorded distance, update the shortest distance.
  6. Continue exploring the maze, always choosing locations that haven't been visited or have a shorter path available.
  7. Stop when you reach the hole, or when all reachable locations have been explored.
  8. If you reached the hole, the shortest distance recorded is the answer. If not, there is no path to the hole.

Code Implementation

def shortest_distance(maze, start, destination):
    rows = len(maze)
    cols = len(maze[0])
    start_row, start_col = start
    destination_row, destination_col = destination

    distance = [[float('inf')] * cols for _ in range(rows)]
    distance[start_row][start_col] = 0

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

    queue = [(start_row, start_col)]

    while queue:
        current_row, current_col = queue.pop(0)

        for delta_row, delta_col in directions:
            row = current_row
            col = current_col
            step_distance = 0

            while 0 <= row + delta_row < rows and 0 <= col + delta_col < cols and \
                    maze[row + delta_row][col + delta_col] == 0:
                row += delta_row
                col += delta_col
                step_distance += 1

            # Check if a shorter path to (row, col) is found
            if distance[current_row][current_col] + step_distance < distance[row][col]:

                distance[row][col] = distance[current_row][current_col] + step_distance
                queue.append((row, col))

    # Check if the destination is reachable
    if distance[destination_row][destination_col] == float('inf'):
        return -1
    else:
        return distance[destination_row][destination_col]

Big(O) Analysis

Time Complexity
O(m * n * max(m, n))The algorithm uses a priority queue (or similar data structure) to explore the maze. In the worst case, it might visit each cell in the maze, which has dimensions m x n, multiple times. For each cell, rolling the ball to the next wall in each of the four directions takes at most max(m, n) steps. Therefore, the overall time complexity is proportional to the number of cells multiplied by the maximum distance the ball rolls in one of the four directions from any particular cell, yielding O(m * n * max(m, n)).
Space Complexity
O(m*n)The algorithm uses a distance array of the same dimensions as the maze (m rows and n columns) to keep track of the shortest distances found so far to each location, as described in step 4. It also implicitly uses a queue or stack (depending on the implementation of the exploration) to store the locations to be visited, which in the worst case, could contain all the cells of the maze. Therefore, the auxiliary space is proportional to the size of the maze, which is m*n, leading to a space complexity of O(m*n).

Edge Cases

CaseHow to Handle
Null or empty mazeReturn -1 immediately since no path exists.
Start and goal are the same cellReturn 0 immediately if the start and goal are the same.
Maze is a single cellReturn 0 if the start and end are the same cell, otherwise -1 if they are different.
Maze contains obstacles blocking the pathThe algorithm should correctly navigate around obstacles, finding the shortest path if it exists.
No path exists between start and goalReturn -1 when the priority queue becomes empty indicating no path.
Large maze dimensions that can lead to memory issues or slow performance with inefficient searchUsing Dijkstra's algorithm with a priority queue can efficiently find the shortest path.
Integer overflow when calculating distancesUse long data type to store distances to prevent potential overflow during accumulation.
Invalid start or goal coordinates (out of bounds)Return -1 immediately if start or goal coordinates are invalid.