Taro Logo

Snakes and Ladders

Medium
Google logo
Google
2 views
Topics:
GraphsArrays

You are given an n x n integer matrix board representing a Snakes and Ladders board game. The cells are labeled from 1 to n^2 in a boustrophedon (snake-like) pattern, starting from the bottom left.

You start on square 1. In each move, you roll a standard 6-sided die and move up to 6 squares. If you land on a square with a snake or ladder, you must move to the destination of that snake or ladder. The game ends when you reach square n^2.

Return the least number of dice rolls required to reach square n^2. If it is not possible to reach the square, return -1.

Example:

Consider the following board:

[[-1,-1,-1,-1,-1,-1],
 [-1,-1,-1,-1,-1,-1],
 [-1,-1,-1,-1,-1,-1],
 [-1,35,-1,-1,13,-1],
 [-1,-1,-1,-1,-1,-1],
 [-1,15,-1,-1,-1,-1]]

Starting from square 1:

  1. Roll a 1 to reach square 2, which has a ladder to square 15.
  2. Roll a 2 to reach square 17, which has a snake to square 13.
  3. Roll a 1 to reach square 14, which has a ladder to square 35.
  4. Roll a 1 to reach square 36.

The minimum number of moves is 4.

Constraints:

  • 2 <= n <= 20
  • board[i][j] is either -1 or in the range [1, n^2].
  • Squares 1 and n^2 do not have snakes or ladders.

Can you provide an efficient algorithm to solve this problem?

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 board (N x N), and what is the range of values for N?
  2. How are snakes and ladders represented? Specifically, if a cell contains a snake or ladder, what value will be stored there, and how does it map to the destination cell?
  3. Is it possible for a snake or ladder to start or end on the first or last cell (cell 1 or cell N*N)?
  4. If it's impossible to reach the end, what value should I return?
  5. Are there any negative cycles possible within the snake and ladder configurations (e.g., a snake leads to a ladder which leads back to the snake's starting point)? If so, how should I handle them?

Brute Force Solution

Approach

The brute force approach to Snakes and Ladders involves exploring every possible path a player can take on the board. We will simulate every possible sequence of dice rolls and moves until we find the shortest path to the end. It's like trying out every single option to guarantee finding the solution.

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

  1. Start at square one.
  2. Consider all six possible dice rolls (1 through 6).
  3. For each possible roll, move your piece accordingly, taking into account any snakes or ladders you land on.
  4. Repeat steps two and three for each new square you land on, considering all possible subsequent dice rolls from that square.
  5. Keep track of every possible sequence of moves and how many dice rolls it took to reach that square.
  6. Continue exploring paths until you reach the final square.
  7. Compare all the sequences of moves that led to the final square.
  8. Choose the sequence that used the fewest dice rolls; this is the shortest path.

Code Implementation

def snakes_and_ladders_brute_force(board, number_of_rows, number_of_columns):

    board_size = number_of_rows * number_of_columns
    queue = [(1, 0)]
    visited = {1}

    while queue:
        current_square, moves = queue.pop(0)

        if current_square == board_size:
            return moves

        # Consider all possible dice rolls (1 through 6).
        for dice_roll in range(1, 7):
            next_square = current_square + dice_roll

            if next_square <= board_size:
                # Account for snakes and ladders
                row = (next_square - 1) // number_of_columns
                col = (next_square - 1) % number_of_columns
                if number_of_rows % 2 == 0:
                    if row % 2 == 0:
                        board_index = next_square - 1
                    else:
                        board_index = (row + 1) * number_of_columns - col - 1
                else:
                    if row % 2 == 0:
                        board_index = (row + 1) * number_of_columns - col - 1
                    else:
                        board_index = next_square - 1

                if board[number_of_rows - 1 - (board_index // number_of_columns)][board_index % number_of_columns] != -1:
                    next_square = board[number_of_rows - 1 - (board_index // number_of_columns)][board_index % number_of_columns]

                # Only explore unvisited squares.
                if next_square not in visited:
                    visited.add(next_square)
                    queue.append((next_square, moves + 1))

    # If no path is found, return -1.
    return -1

Big(O) Analysis

Time Complexity
O(6^n)The brute force approach explores every possible sequence of dice rolls. In the worst case, the algorithm needs to consider all possible paths to the end of the board. Since each square can have up to 6 possible next moves (corresponding to dice rolls 1-6), and in the worst case we might need to explore paths of length n (where n is the number of squares on the board), the total number of paths explored grows exponentially with the number of squares. This means the runtime is proportional to 6 raised to the power of n, resulting in O(6^n) time complexity.
Space Complexity
O(N)The brute force approach explores every possible path using a breadth-first search implicitly. It keeps track of visited squares to avoid cycles and stores the number of dice rolls to reach each square. This requires an auxiliary queue (or equivalent data structure for breadth-first search) and a visited array, both of which can grow up to the size of the board, N, where N is the number of squares on the board. Therefore, the auxiliary space complexity is O(N).

Optimal Solution

Approach

The goal is to find the shortest path to the end of the board. We can treat the board as a graph and use a common graph search algorithm to find the quickest route.

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

  1. Think of each square on the board as a place you can be, and moving from one square to another as a step you can take.
  2. Imagine each square is connected to the next six squares (representing a die roll). If there's a snake or ladder, the connection goes to the square at the end of it instead.
  3. Start at the first square. Use a method that explores possible moves, like a breadth-first search. This helps you discover all reachable squares step-by-step.
  4. Keep track of how many dice rolls it takes to reach each square. That way, when you reach the final square, you'll know the smallest number of rolls it took.
  5. When you find a snake or ladder, immediately jump to the square it takes you to, and continue counting from there.
  6. Continue this process until you reach the final square. The number of rolls it took is the answer. If you explore every possible path without reaching the final square, then it is not possible to win the game.

Code Implementation

from collections import deque

def snakes_and_ladders(board):
    board_size = len(board)
    squares = board_size * board_size

    def get_coordinates(square_number):
        row = (square_number - 1) // board_size
        col = (square_number - 1) % board_size

        row = board_size - 1 - row
        if (board_size % 2 == 0 and (board_size - 1 - row) % 2 != 0) or \
           (board_size % 2 != 0 and (board_size - 1 - row) % 2 == 0):
            col = board_size - 1 - col

        return row, col

    queue = deque([(1, 0)])
    visited = {1}

    while queue:
        current_square, moves_made = queue.popleft()

        if current_square == squares:
            return moves_made

        for dice_roll in range(1, 7):
            next_square = current_square + dice_roll

            if next_square > squares:
                continue

            row, col = get_coordinates(next_square)
            # Account for snakes and ladders
            if board[row][col] != -1:
                next_square = board[row][col]

            if next_square not in visited:
                visited.add(next_square)
                # Breadth-first search guarantees shortest path
                queue.append((next_square, moves_made + 1))

    return -1

Big(O) Analysis

Time Complexity
O(n)The provided solution uses Breadth-First Search (BFS) on a graph where each square of the board is a node. In the worst-case scenario, BFS visits each square on the board once to determine the shortest path to the destination. Therefore, the algorithm explores all 'n' squares, where n represents the total number of squares on the board. Each square is enqueued and dequeued only once. Thus the time complexity is O(n).
Space Complexity
O(N)The breadth-first search algorithm uses a queue to store the squares to visit, which in the worst case, could contain all N squares of the board. Additionally, we need to keep track of visited squares, which could be done using a boolean array of size N. Therefore, the auxiliary space used is proportional to the number of squares on the board, N, resulting in O(N) space complexity.

Edge Cases

CaseHow to Handle
Null or empty boardReturn -1 immediately as no game can be played.
Board size of 1x1Return 0 immediately since the start is already the end.
No ladders or snakes on the boardThe shortest path will be the number of cells minus 1, proceed with standard BFS.
Start and end are directly connected by a ladder or snakeThe BFS algorithm will prioritize this direct connection.
Board filled entirely with snakes leading to the startThe algorithm should correctly determine that the end cannot be reached and return -1.
Cyclic snakes and ladders leading nowhereThe visited set prevents infinite loops, allowing the algorithm to terminate if there's no path.
Integer overflow when calculating the next cell positionUse long data type to store cell positions before converting to row and column.
Maximum sized board (e.g., 20x20)The BFS will still work, but the time and memory complexity may increase depending on the distribution of snakes and ladders; analyze for potential optimizations.