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 number1
- 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 betweencurr + 1
andmin(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 byboard[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 square2
, and square2
has a ladder to square3
, you go to square3
but don't follow the subsequent ladder from square3
to square4
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.
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:
- 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.
- 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 ordervis
(set): Tracks visited squares to avoid revisiting themans
: Counter for the number of dice rolls (levels in BFS)
Step-by-step Implementation:
-
Initialization: Start with square 1 in the queue and mark it as visited. Set
ans = 0
for counting rolls, and calculatem = n * n
as the target square. -
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.
-
Check for Target: If we've reached square
m
(which isnΒ²
), return the current number of rolls. -
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.
-
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
-
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 aty
. -
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.
-
Increment Roll Count: After processing all squares at the current level, increment
ans
to move to the next level. -
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 EvaluatorExample 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:
i, j = divmod(5-1, 3)
βi=1, j=1
(logical position)- Row 1 is odd (0-indexed), so reverse column:
j = 3-1-1 = 1
(stays same) - Flip row for bottom-up:
i = 3-1-1 = 1
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:
- The
vis
set stores visited positions, which in the worst case contains allnΒ²
cells on the board, requiringO(nΒ²)
space. - The queue
q
for BFS can contain at mostO(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]
Which of the following array represent a max heap?
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!