Facebook Pixel

909. Snakes and Ladders

Problem Description

You are given an n x n integer matrix board representing a Snakes and Ladders game board. The cells are numbered from 1 to nΒ² in a special pattern called "Boustrophedon style":

  • Start from the bottom-left corner (board[n-1][0]) with number 1
  • Fill numbers left-to-right for the bottom row
  • For the next row up, fill numbers right-to-left
  • Continue alternating direction for each row moving upward

You start at square 1 and want to reach square nΒ². On each turn:

  • Roll a 6-sided die to move forward between 1 to 6 squares
  • From current square curr, you can move to any square numbered between curr + 1 and min(curr + 6, nΒ²)
  • If you land on a square with a snake or ladder (where board[r][c] != -1), you must immediately move to the destination square indicated by board[r][c]
  • If you land on a square without a snake or ladder (board[r][c] == -1), you stay on that square
  • Important: You only take a snake or ladder once per dice roll - if the destination has another snake or ladder, you don't follow it

The goal is to find the minimum number of dice rolls needed to reach square nΒ². If it's impossible to reach the final square, return -1.

For example, in a 2x2 board [[-1,4],[-1,3]]:

  • Squares are numbered: [[4,3],[1,2]] (following Boustrophedon pattern)
  • If you roll to move from square 1 to square 2, and square 2 has a ladder to square 3, you go to square 3 but don't follow the subsequent ladder from square 3 to square 4

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: The Snakes and Ladders board can be viewed as a graph where each square is a node, and edges exist from each square to the squares reachable by rolling a die (1-6 steps forward). Snakes and ladders create additional directed edges.

Is it a tree?

  • No: The graph is not a tree because there can be multiple paths to reach the same square (through different dice rolls or via snakes/ladders), creating cycles in the graph structure.

Is the problem related to directed acyclic graphs?

  • No: The graph contains cycles (you can potentially reach the same square through different paths), so it's not a DAG.

Is the problem related to shortest paths?

  • Yes: We need to find the minimum number of dice rolls (edges traversed) to reach from square 1 to square nΒ², which is essentially finding the shortest path in an unweighted graph.

Is the graph Weighted?

  • No: Each move (dice roll) has the same cost of 1, regardless of how many squares you advance or whether you take a snake/ladder. This makes it an unweighted graph problem.

BFS

  • Yes: For unweighted shortest path problems, BFS is the optimal choice.

Conclusion: The flowchart suggests using BFS (Breadth-First Search) for solving the Snakes and Ladders problem, which aligns perfectly with finding the minimum number of moves in an unweighted graph.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that this problem is fundamentally about finding the shortest path in a graph. Each square on the board is a node, and each possible move (rolling the die) creates an edge to another node.

Why BFS works perfectly here:

  1. Level-by-level exploration: BFS explores all squares reachable in 1 roll, then all squares reachable in 2 rolls, and so on. This guarantees we find the minimum number of rolls.
  2. Unweighted edges: Each dice roll counts as exactly 1 move, regardless of whether we move 1 square or 6 squares, or take a snake/ladder. This makes it an unweighted graph problem where BFS finds the optimal solution.

The tricky parts to handle:

  • Board numbering: The Boustrophedon (zigzag) pattern means we need to convert between square numbers (1 to nΒ²) and board coordinates [row][column]. For odd-numbered rows from the bottom, we read right-to-left.
  • Snakes and ladders: These are just "teleportation" edges. When we land on a square with value β‰  -1, we immediately jump to that destination square instead.
  • One-time teleportation: We only follow one snake/ladder per dice roll. If the destination has another snake/ladder, we don't chain them.

The BFS approach naturally handles all these complexities:

  • Start from square 1
  • For each square we can currently reach, try all possible dice rolls (1-6)
  • If we land on a snake/ladder, go to its destination
  • Mark visited squares to avoid redundant exploration
  • The first time we reach square nΒ², we've found the shortest path

This is more efficient than DFS because DFS might explore longer paths first, while BFS guarantees we find the shortest path as soon as we reach the destination.

Learn more about Breadth-First Search patterns.

Solution Approach

The implementation follows a standard BFS pattern with some special handling for the board's unique numbering system.

Data Structures Used:

  • q (deque): A queue to store squares to be explored in BFS order
  • vis (set): Tracks visited squares to avoid revisiting them
  • ans: Counter for the number of dice rolls (levels in BFS)

Step-by-step Implementation:

  1. Initialization: Start with square 1 in the queue and mark it as visited. Set ans = 0 for counting rolls, and calculate m = n * n as the target square.

  2. BFS Level Processing: Process all squares at the current level (same number of rolls):

    for _ in range(len(q)):
        x = q.popleft()

    This ensures we process all squares reachable with the current number of rolls before incrementing.

  3. Check for Target: If we've reached square m (which is nΒ²), return the current number of rolls.

  4. Explore Next Moves: From square x, try all possible dice rolls (1 to 6):

    for y in range(x + 1, min(x + 6, m) + 1):

    This generates all reachable squares from the current position.

  5. Convert Square Number to Board Coordinates: The tricky part - converting square number y to board position [i][j]:

    i, j = divmod(y - 1, n)  # Get row and column in logical order
    if i & 1:                # If odd row (0-indexed)
        j = n - j - 1        # Reverse column for zigzag pattern
    i = n - i - 1           # Flip row (board is bottom-up)
    • divmod(y - 1, n) gives us the logical row and column (0-indexed)
    • Odd rows (when i & 1 is true) go right-to-left, so we reverse the column
    • Rows are numbered bottom-to-top, so we flip the row index
  6. Handle Snakes/Ladders: Check if the square has a snake or ladder:

    z = y if board[i][j] == -1 else board[i][j]

    If board[i][j] != -1, we must move to that destination instead of staying at y.

  7. Add to Queue if Unvisited: Only explore squares we haven't visited:

    if z not in vis:
        vis.add(z)
        q.append(z)

    This prevents infinite loops and redundant exploration.

  8. Increment Roll Count: After processing all squares at the current level, increment ans to move to the next level.

  9. Return -1 if Unreachable: If the queue becomes empty without reaching the target, return -1.

The beauty of this BFS approach is that it guarantees the minimum number of rolls because it explores all possibilities level by level, and the first time we reach the target is guaranteed to be via the shortest path.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with a 3x3 board to illustrate the solution approach:

Board: [[-1,-1,-1],
        [-1, 9, 8],
        [-1,-1, 2]]

Square numbering (Boustrophedon style):
[7, 8, 9]  ← Row 2 (top)
[6, 5, 4]  β†’ Row 1 (middle) 
[1, 2, 3]  ← Row 0 (bottom)

Board with snakes/ladders marked:
- Square 5 has a ladder to square 9
- Square 6 has a ladder to square 8
- Square 3 has a snake to square 2

BFS Execution:

Initial State:

  • Queue: [1]
  • Visited: {1}
  • Rolls: 0

Roll 1 (Process square 1):

  • From square 1, we can reach squares 2-7 (roll 1-6)
  • Check each destination:
    • Square 2: No snake/ladder β†’ stay at 2 β†’ add to queue
    • Square 3: Has snake to 2 β†’ already visited 2 β†’ skip
    • Square 4: No snake/ladder β†’ stay at 4 β†’ add to queue
    • Square 5: Has ladder to 9 β†’ go to 9 β†’ add to queue βœ“
    • Square 6: Has ladder to 8 β†’ go to 8 β†’ add to queue
    • Square 7: No snake/ladder β†’ stay at 7 β†’ add to queue
  • Queue: [2, 4, 9, 8, 7]
  • Visited: {1, 2, 4, 9, 8, 7}
  • Rolls: 1

Check: Square 9 is in our queue! We reached the target in just 1 roll.

Answer: 1 roll

The key insight: Even though square 5 is at position 5, its ladder takes us directly to square 9 (the target) in just one dice roll. This demonstrates how BFS efficiently finds the shortest path by exploring all possibilities at each level.

Coordinate Conversion Example: Let's see how square 5 converts to board coordinates:

  1. i, j = divmod(5-1, 3) β†’ i=1, j=1 (logical position)
  2. Row 1 is odd (0-indexed), so reverse column: j = 3-1-1 = 1 (stays same)
  3. Flip row for bottom-up: i = 3-1-1 = 1
  4. board[1][1] = 9 β†’ ladder to square 9

This walkthrough shows how BFS naturally handles the complex board navigation and finds the optimal solution.

Solution Implementation

1class Solution:
2    def snakesAndLadders(self, board: List[List[int]]) -> int:
3        board_size = len(board)
4      
5        # BFS queue starting from position 1
6        queue = deque([1])
7      
8        # Track visited positions to avoid cycles
9        visited = {1}
10      
11        # Number of moves taken
12        moves = 0
13      
14        # Total number of cells on the board
15        total_cells = board_size * board_size
16      
17        # BFS to find minimum moves to reach the last cell
18        while queue:
19            # Process all positions at current distance level
20            for _ in range(len(queue)):
21                current_position = queue.popleft()
22              
23                # Check if we've reached the destination
24                if current_position == total_cells:
25                    return moves
26              
27                # Try all possible dice rolls (1 to 6)
28                for dice_roll in range(1, 7):
29                    next_position = current_position + dice_roll
30                  
31                    # Don't go beyond the board
32                    if next_position > total_cells:
33                        break
34                  
35                    # Convert position number to board coordinates
36                    # Position numbering starts from 1, so subtract 1 for 0-based indexing
37                    row_from_bottom, column = divmod(next_position - 1, board_size)
38                  
39                    # Handle zigzag pattern: odd rows (from bottom) go right-to-left
40                    if row_from_bottom & 1:  # Odd row from bottom
41                        column = board_size - column - 1
42                  
43                    # Convert row index from bottom to top (board is indexed top-to-bottom)
44                    row_from_top = board_size - row_from_bottom - 1
45                  
46                    # Check for snake or ladder at this position
47                    # If board value is -1, no snake/ladder; otherwise use the destination
48                    final_position = next_position if board[row_from_top][column] == -1 else board[row_from_top][column]
49                  
50                    # Add to queue if not visited
51                    if final_position not in visited:
52                        visited.add(final_position)
53                        queue.append(final_position)
54          
55            # Increment move counter after processing all positions at current distance
56            moves += 1
57      
58        # If we exit the loop without finding the destination, it's unreachable
59        return -1
60
1class Solution {
2    public int snakesAndLadders(int[][] board) {
3        int boardSize = board.length;
4        int totalSquares = boardSize * boardSize;
5      
6        // BFS queue to store positions to explore
7        Deque<Integer> queue = new ArrayDeque<>();
8        queue.offer(1); // Start from square 1
9      
10        // Track visited squares to avoid revisiting
11        boolean[] visited = new boolean[totalSquares + 1];
12        visited[1] = true;
13      
14        // BFS level by level (each level represents one dice roll)
15        int moves = 0;
16        while (!queue.isEmpty()) {
17            int levelSize = queue.size();
18          
19            // Process all positions at current level
20            for (int i = 0; i < levelSize; i++) {
21                int currentPosition = queue.poll();
22              
23                // Check if we reached the final square
24                if (currentPosition == totalSquares) {
25                    return moves;
26                }
27              
28                // Try all possible dice rolls (1 to 6)
29                for (int diceRoll = 1; diceRoll <= 6; diceRoll++) {
30                    int nextPosition = currentPosition + diceRoll;
31                  
32                    // Don't go beyond the last square
33                    if (nextPosition > totalSquares) {
34                        break;
35                    }
36                  
37                    // Convert linear position to board coordinates
38                    // Calculate row and column based on Boustrophedon style
39                    int row = (nextPosition - 1) / boardSize;
40                    int col = (nextPosition - 1) % boardSize;
41                  
42                    // Odd rows (0-indexed) go right to left
43                    if (row % 2 == 1) {
44                        col = boardSize - col - 1;
45                    }
46                  
47                    // Board is indexed from bottom to top
48                    row = boardSize - row - 1;
49                  
50                    // Check for snake or ladder at this position
51                    int destination = board[row][col] == -1 ? nextPosition : board[row][col];
52                  
53                    // Add to queue if not visited
54                    if (!visited[destination]) {
55                        visited[destination] = true;
56                        queue.offer(destination);
57                    }
58                }
59            }
60            moves++;
61        }
62      
63        // No path found to reach the final square
64        return -1;
65    }
66}
67
1class Solution {
2public:
3    int snakesAndLadders(vector<vector<int>>& board) {
4        int boardSize = board.size();
5        int totalSquares = boardSize * boardSize;
6      
7        // BFS queue starting from square 1
8        queue<int> bfsQueue{{1}};
9      
10        // Track visited squares to avoid revisiting
11        vector<bool> visited(totalSquares + 1, false);
12        visited[1] = true;
13      
14        // BFS to find minimum moves
15        int moves = 0;
16        while (!bfsQueue.empty()) {
17            int levelSize = bfsQueue.size();
18          
19            // Process all nodes at current level
20            for (int i = 0; i < levelSize; ++i) {
21                int currentSquare = bfsQueue.front();
22                bfsQueue.pop();
23              
24                // Check if we reached the final square
25                if (currentSquare == totalSquares) {
26                    return moves;
27                }
28              
29                // Try all possible dice rolls (1 to 6)
30                for (int nextSquare = currentSquare + 1; 
31                     nextSquare <= min(currentSquare + 6, totalSquares); 
32                     ++nextSquare) {
33                  
34                    // Convert square number to board coordinates
35                    // Row calculation: counting from bottom
36                    int row = (nextSquare - 1) / boardSize;
37                    int col = (nextSquare - 1) % boardSize;
38                  
39                    // Handle zigzag pattern: odd rows go right-to-left
40                    if (row % 2 == 1) {
41                        col = boardSize - col - 1;
42                    }
43                  
44                    // Convert to actual board row (board is indexed top-to-bottom)
45                    row = boardSize - row - 1;
46                  
47                    // Check for snake or ladder, otherwise use the square itself
48                    int destination = (board[row][col] == -1) ? nextSquare : board[row][col];
49                  
50                    // Add unvisited destination to queue
51                    if (!visited[destination]) {
52                        visited[destination] = true;
53                        bfsQueue.push(destination);
54                    }
55                }
56            }
57          
58            // Increment move count after processing current level
59            ++moves;
60        }
61      
62        // No path found to reach the final square
63        return -1;
64    }
65};
66
1/**
2 * Finds the minimum number of moves to reach the end of a Snakes and Ladders board
3 * using BFS (Breadth-First Search) approach
4 * @param board - N x N board where board[i][j] represents snake/ladder destination or -1
5 * @returns Minimum number of moves to reach the end, or -1 if impossible
6 */
7function snakesAndLadders(board: number[][]): number {
8    const boardSize = board.length;
9    const targetSquare = boardSize * boardSize;
10  
11    // Initialize BFS queue with starting position 1
12    let currentLevelQueue: number[] = [1];
13  
14    // Track visited squares to avoid revisiting
15    const visited: boolean[] = Array(targetSquare + 1).fill(false);
16    visited[1] = true;
17  
18    // BFS level-by-level traversal
19    for (let moveCount = 0; currentLevelQueue.length > 0; moveCount++) {
20        const nextLevelQueue: number[] = [];
21      
22        // Process all positions in current level
23        for (const currentPosition of currentLevelQueue) {
24            // Check if we've reached the target
25            if (currentPosition === targetSquare) {
26                return moveCount;
27            }
28          
29            // Try all possible dice rolls (1 to 6)
30            for (let nextPosition = currentPosition + 1; 
31                 nextPosition <= Math.min(currentPosition + 6, targetSquare); 
32                 nextPosition++) {
33              
34                // Convert linear position to board coordinates
35                // Row index (from bottom)
36                let rowIndex = Math.floor((nextPosition - 1) / boardSize);
37                // Column index
38                let columnIndex = (nextPosition - 1) % boardSize;
39              
40                // Handle alternating row direction (Boustrophedon style)
41                // Odd rows are traversed right-to-left
42                if (rowIndex % 2 === 1) {
43                    columnIndex = boardSize - columnIndex - 1;
44                }
45              
46                // Convert to actual board coordinates (board is stored top-to-bottom)
47                rowIndex = boardSize - rowIndex - 1;
48              
49                // Get destination after snake/ladder (or stay at current position if -1)
50                const destination = board[rowIndex][columnIndex] === -1 
51                    ? nextPosition 
52                    : board[rowIndex][columnIndex];
53              
54                // Add unvisited destinations to next level
55                if (!visited[destination]) {
56                    visited[destination] = true;
57                    nextLevelQueue.push(destination);
58                }
59            }
60        }
61      
62        // Move to next level
63        currentLevelQueue = nextLevelQueue;
64    }
65  
66    // Target square is unreachable
67    return -1;
68}
69

Time and Space Complexity

Time Complexity: O(nΒ²)

The algorithm uses BFS to traverse the board. In the worst case, we visit each cell on the board exactly once. Since the board has n Γ— n = nΒ² cells, and for each cell we explore up to 6 possible next positions (dice rolls 1-6), the total operations are bounded by O(nΒ² Γ— 6) = O(nΒ²). The coordinate conversion operations divmod and index calculations are O(1) operations.

Space Complexity: O(nΒ²)

The space complexity comes from two main sources:

  1. The vis set stores visited positions, which in the worst case contains all nΒ² cells on the board, requiring O(nΒ²) space.
  2. The queue q for BFS can contain at most O(nΒ²) elements in the worst case when multiple cells are reachable at the same level.

Therefore, the total space complexity is O(nΒ²), where n is the length of the side of the board.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Boustrophedon (Zigzag) Pattern Conversion

The most common pitfall is incorrectly converting between the square number (1 to nΒ²) and the board coordinates [row][column]. Many developers make mistakes in:

  • Forgetting that odd rows (from the bottom) go right-to-left
  • Miscalculating which rows should be reversed
  • Confusing 0-based vs 1-based indexing

Wrong Implementation:

# Common mistake: not handling the zigzag pattern correctly
row = (position - 1) // n
col = (position - 1) % n
# Missing the reversal for odd rows!

Correct Implementation:

row_from_bottom, col = divmod(position - 1, n)
if row_from_bottom & 1:  # Odd row from bottom
    col = n - col - 1     # Reverse column for right-to-left
row_from_top = n - row_from_bottom - 1  # Convert to top-down indexing

2. Following Multiple Snakes/Ladders in One Move

Another critical pitfall is recursively following snakes/ladders. When you land on a square with a snake/ladder, you should move to its destination, but if that destination also has a snake/ladder, you should NOT follow it.

Wrong Implementation:

final_position = next_position
while board[row][col] != -1:
    final_position = board[row][col]
    # Recalculate row, col for final_position
    # This is WRONG - only follow one snake/ladder per dice roll

Correct Implementation:

# Only check once - don't chain snakes/ladders
final_position = next_position if board[row][col] == -1 else board[row][col]

3. Visiting Logic Errors

Marking squares as visited at the wrong time can lead to incorrect results or missing optimal paths.

Wrong Implementation:

# Marking visited AFTER popping from queue
current = queue.popleft()
visited.add(current)  # Too late! Might add duplicates to queue

Correct Implementation:

# Mark as visited when ADDING to queue
if final_position not in visited:
    visited.add(final_position)  # Mark immediately
    queue.append(final_position)

4. Off-by-One Errors in Dice Roll Range

The dice roll should allow moves to squares [current + 1, min(current + 6, nΒ²)], inclusive.

Wrong Implementation:

# Missing the +1 for inclusive range
for dice in range(1, 7):
    next_pos = current + dice
    if next_pos > n * n:  # Should use >= or continue instead of break
        break

Correct Implementation:

for dice in range(1, 7):
    next_pos = current + dice
    if next_pos > n * n:
        break  # Can break since subsequent rolls will also exceed

5. Testing Your Coordinate Conversion

A helpful debugging technique is to verify your coordinate conversion with a simple test:

def verify_conversion(n):
    """Print the board with square numbers to verify conversion logic"""
    board_numbers = [[0] * n for _ in range(n)]
    for num in range(1, n * n + 1):
        row_from_bottom, col = divmod(num - 1, n)
        if row_from_bottom & 1:
            col = n - col - 1
        row_from_top = n - row_from_bottom - 1
        board_numbers[row_from_top][col] = num
  
    # Should show the Boustrophedon pattern
    for row in board_numbers:
        print(row)

For a 3x3 board, this should output:

[7, 8, 9]
[6, 5, 4]
[1, 2, 3]
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following array represent a max heap?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More