Snakes and Ladders
MedPrompt
You are given an n x n integer matrix board where the cells are labeled from 1 to n2 in a Boustrophedon style starting from the bottom left of the board (i.e. board[n - 1][0]) and alternating direction each row.
You start on square 1 of the board. In each move, starting from square curr, do the following:
- Choose a destination square
nextwith a label in the range[curr + 1, min(curr + 6, n2)].- This choice simulates the result of a standard 6-sided die roll: i.e., there are always at most 6 destinations, regardless of the size of the board.
- If
nexthas a snake or ladder, you must move to the destination of that snake or ladder. Otherwise, you move tonext. - The game ends when you reach the square
n2.
A board square on row r and column c has a snake or ladder if board[r][c] != -1. The destination of that snake or ladder is board[r][c]. Squares 1 and n2 are not the starting points of any snake or ladder.
Note that you only take a snake or ladder at most once per dice roll. If the destination to a snake or ladder is the start of another snake or ladder, you do not follow the subsequent snake or ladder.
- For example, suppose the board is
[[-1,4],[-1,3]], and on the first move, your destination square is2. You follow the ladder to square3, but do not follow the subsequent ladder to4.
Return the least number of dice rolls required to reach the square n2. If it is not possible to reach the square, return -1.
Example 1:
Input: 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]]
Output: 4
Explanation:
In the beginning, you start at square 1 (at row 5, column 0).
You decide to move to square 2 and must take the ladder to square 15.
You then decide to move to square 17 and must take the snake to square 13.
You then decide to move to square 14 and must take the ladder to square 35.
You then decide to move to square 36, ending the game.
This is the lowest possible number of moves to reach the last square, so return 4.Example 2:
Input: board = [[-1,-1],[-1,3]]
Output: 1
Constraints:
n == board.length == board[i].length2 <= n <= 20board[i][j]is either-1or in the range[1, n2].- The squares labeled
1andn2are not the starting points of any snake or ladder.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach explores the game by trying every possible sequence of dice rolls recursively. It's a brute-force method that attempts to find a path to the destination square n*n. To find the shortest path, it must explore all possible paths from the start to the end and keep track of the minimum number of moves found so far. This is fundamentally inefficient because it doesn't prioritize shorter paths and explores many dead-ends and suboptimal routes.
Algorithm
- Initialize a global variable
min_movesto infinity. - Create a helper function
getCoordinates(square)to convert a square number to its(row, col)coordinates. - Define a recursive function
dfs(square, moves, visited_path). - Inside
dfs:- If the current number of
movesis already greater than or equal tomin_moves, we can prune this path and return, as it won't be a better solution. - If
squareis the targetn*n, we have found a path. We updatemin_moves = min(min_moves, moves)and return. - Mark the current
squareas visited in the current path to avoid cycles within a single path exploration. - Iterate through each possible dice roll
dfrom 1 to 6:- Calculate
next_square = square + d. - If
next_squareexceedsn*n, stop exploring further dice rolls from this square. - Use the helper function to get the coordinates for
next_square. - Check the board for a snake or ladder to determine the
final_dest. - If
final_desthas not been visited in the current path, make a recursive call:dfs(final_dest, moves + 1, visited_path).
- Calculate
- Unmark
squarefromvisited_pathto allow other paths to visit this square (this is called backtracking).
- If the current number of
- Start the search by calling
dfs(1, 0, new HashSet<>()). - After the search completes, if
min_movesis still infinity, it means the destination was unreachable, so return -1. Otherwise, returnmin_moves.
Walkthrough
We define a recursive function, say dfs(currentSquare, currentMoves), that explores paths from the currentSquare. The base case for the recursion is when currentSquare reaches n*n, at which point we compare currentMoves with a global minimum and update it if the current path is shorter. To prevent infinite loops (e.g., a snake leading back to a square we've already been on in the current path), we need to keep track of the squares visited along the current recursive path.
The function iterates through all possible next moves (dice rolls 1 to 6). For each move, it calculates the destination square, accounting for any snakes or ladders. It then makes a recursive call for the new square with an incremented move count. This approach is very inefficient because it explores paths in a depth-first manner, meaning it might explore a very long path completely before exploring a much shorter one. It essentially performs an exhaustive search of the state space, which is exponential in nature.
class Solution { int minMoves = Integer.MAX_VALUE; int n; public int snakesAndLadders(int[][] board) { this.n = board.length; // Using a boolean array for the visited path for simplicity dfs(1, 0, new boolean[n * n + 1], board); return minMoves == Integer.MAX_VALUE ? -1 : minMoves; } private void dfs(int currentSquare, int moves, boolean[] visitedOnPath, int[][] board) { // Pruning: if we've already taken more moves than the best solution found so far if (moves >= minMoves) { return; } // Mark the current square as visited for this path visitedOnPath[currentSquare] = true; for (int i = 1; i <= 6; i++) { int nextSquare = currentSquare + i; if (nextSquare > n * n) { break; } int[] coords = getCoordinates(nextSquare); int r = coords[0]; int c = coords[1]; int finalDestination = board[r][c] == -1 ? nextSquare : board[r][c]; if (finalDestination == n * n) { minMoves = Math.min(minMoves, moves + 1); // We can continue checking other dice rolls, as a shorter path might exist // from the current square, though this is unlikely with this problem's structure. continue; } if (!visitedOnPath[finalDestination]) { dfs(finalDestination, moves + 1, visitedOnPath, board); } } // Backtrack: un-mark the square so other paths can use it visitedOnPath[currentSquare] = false; } private int[] getCoordinates(int square) { int r = n - 1 - (square - 1) / n; int c = (square - 1) % n; if ((n - 1 - r) % 2 == 1) { // Odd row from top (0-indexed) c = n - 1 - c; } return new int[]{r, c}; }}Complexity
Time
O(6^D), where D is the length of the longest simple path. In the worst case, this is exponential in N^2, making it infeasible for all but the smallest boards.
Space
O(N^2) for the recursion stack depth in the worst case, where N is the side length of the board. The `visitedOnPath` array also takes O(N^2) space.
Trade-offs
Pros
Conceptually simple to understand as it mimics trying out all possibilities.
Cons
Extremely inefficient and will likely result in a "Time Limit Exceeded" error for most inputs.
Does not find the shortest path first; it must explore a potentially large number of paths to guarantee the minimum.
The state space can be very large, leading to deep recursion and potential stack overflow errors on large boards.
This problem can be modeled as finding the shortest path in an unweighted graph. Each square on the board is a node, and a move (a dice roll followed by a potential snake or ladder) represents an edge. Breadth-First Search (BFS) is the ideal algorithm for this scenario because it explores the graph layer by layer. This guarantees that the first time we reach the destination square n*n, it will be via the path with the minimum number of moves.
Algorithm
- Get the board size
n. - Create a helper function
getCoordinates(square, n)to convert a square number to its(row, col)coordinates. - Initialize a queue
qof integers and add the starting square1. - Initialize a boolean array
visitedof sizen*n + 1and setvisited[1] = trueto avoid cycles and redundant processing. - Initialize a variable
moves = 0. - While the queue
qis not empty:- Get the number of nodes at the current level:
levelSize = q.size(). - Loop
levelSizetimes to process all nodes at the current level before moving to the next.- Dequeue the current square:
curr = q.poll(). - If
curris the target squaren*n, we have found the shortest path. Returnmoves. - For each possible dice roll
dfrom 1 to 6:- Calculate the potential next square:
next = curr + d. - If
next > n*n, break the inner loop as further rolls will also be out of bounds. - Get the board coordinates for
next:(r, c) = getCoordinates(next, n). - Determine the final destination after checking for a snake or ladder:
dest = board[r][c] == -1 ? next : board[r][c]. - If
desthas not been visited (visited[dest]is false):- Mark it as visited:
visited[dest] = true. - Enqueue
destfor processing in the next level:q.add(dest).
- Mark it as visited:
- Calculate the potential next square:
- Dequeue the current square:
- After processing the entire level, increment
moves.
- Get the number of nodes at the current level:
- If the loop finishes (queue becomes empty) and we haven't reached the destination, it's unreachable. Return -1.
Walkthrough
We start at square 1, which is at level 0 of our search. All squares reachable in one move are at level 1, all squares reachable from level 1 squares in one move are at level 2, and so on. BFS systematically explores these levels using a queue.
We use a queue to keep track of the squares to visit next and a visited array to ensure we don't process the same square multiple times, which prevents infinite loops and redundant computations. The search proceeds level by level. In each step, we dequeue all nodes from the current level, find all their unvisited neighbors (squares reachable in one move), and enqueue them. We increment our move counter after each level is fully processed. Because we explore all paths of length k before exploring any path of length k+1, the first time we encounter the target square, we are guaranteed to have done so in the minimum possible number of moves.
import java.util.LinkedList;import java.util.Queue; class Solution { public int snakesAndLadders(int[][] board) { int n = board.length; boolean[] visited = new boolean[n * n + 1]; Queue<Integer> queue = new LinkedList<>(); queue.offer(1); visited[1] = true; int moves = 0; while (!queue.isEmpty()) { int levelSize = queue.size(); for (int i = 0; i < levelSize; i++) { int currentSquare = queue.poll(); if (currentSquare == n * n) { return moves; } for (int j = 1; j <= 6; j++) { int nextSquare = currentSquare + j; if (nextSquare > n * n) { break; } int[] coords = getCoordinates(nextSquare, n); int r = coords[0]; int c = coords[1]; int finalDestination = board[r][c] == -1 ? nextSquare : board[r][c]; if (!visited[finalDestination]) { visited[finalDestination] = true; queue.offer(finalDestination); } } } moves++; } return -1; // Destination not reachable } private int[] getCoordinates(int square, int n) { // Convert 1-based square to 0-based index for calculation int s = square - 1; // Row from top (0-indexed) int rowFromTop = s / n; // Board row (from bottom, 0-indexed) int r = n - 1 - rowFromTop; // Column (0-indexed) int c = s % n; // Adjust column for Boustrophedon order if (rowFromTop % 2 == 1) { // If row from top is odd, direction is right-to-left c = n - 1 - c; } return new int[]{r, c}; }}Complexity
Time
O(N^2), where N is the side length of the board. Each square (node) from 1 to N^2 is enqueued and dequeued at most once. From each square, we perform a constant number of operations (at most 6) to find its neighbors. Therefore, the complexity is proportional to the number of squares.
Space
O(N^2), where N is the side length of the board. The `visited` array requires O(N^2) space. The queue, in the worst case, can also hold up to O(N^2) elements.
Trade-offs
Pros
Guaranteed to find the shortest path in terms of the number of moves for an unweighted graph.
Highly efficient and the optimal approach for this problem.
Cleanly avoids infinite cycles and redundant computations using a
visitedset.
Cons
Requires extra space for the queue and the
visitedarray, which can be up to O(N^2).
Solutions
Solution
class Solution {private int n;public int snakesAndLadders(int[][] board) { n = board.length; Deque<Integer> q = new ArrayDeque<>(); q.offer(1); boolean[] vis = new boolean[n * n + 1]; vis[1] = true; int ans = 0; while (!q.isEmpty()) { for (int t = q.size(); t > 0; --t) { int curr = q.poll(); if (curr == n * n) { return ans; } for (int k = curr + 1; k <= Math.min(curr + 6, n * n); ++k) { int[] p = get(k); int next = k; int i = p[0], j = p[1]; if (board[i][j] != -1) { next = board[i][j]; } if (!vis[next]) { vis[next] = true; q.offer(next); } } } ++ans; } return -1; }private int[] get(int x) { int i = (x - 1) / n, j = (x - 1) % n; if (i % 2 == 1) { j = n - 1 - j; } return new int[]{n - 1 - i, j}; }}class Solution {public: int n; int snakesAndLadders(vector<vector<int>> &board) { n = board.size(); queue<int> q{{1}}; vector<bool> vis(n * n + 1); vis[1] = true; int ans = 0; while (!q.empty()) { for (int t = q.size(); t; --t) { int curr = q.front(); if (curr == n * n) return ans; q.pop(); for (int k = curr + 1; k <= min(curr + 6, n * n); ++k) { auto p = get(k); int next = k; int i = p[0], j = p[1]; if (board[i][j] != -1) next = board[i][j]; if (!vis[next]) { vis[next] = true; q.push(next); } } } ++ans; } return -1; } vector<int> get(int x) { int i = (x - 1) / n, j = (x - 1) % n; if (i % 2 == 1) j = n - 1 - j; return {n - 1 - i, j}; }};class Solution: def snakesAndLadders(self, board: List[List[int]]) -> int: def get(x): i, j = (x - 1) // n, (x - 1) % n if i & 1: j = n - 1 - j return n - 1 - i, j n = len(board) q = deque([1]) vis = {1} ans = 0 while q: for _ in range(len(q)): curr = q . popleft() if curr == n * n: return ans for next in range(curr + 1, min(curr + 7, n * n + 1)): i, j = get(next) if board[i][j] != - 1: next = board[i][j] if next not in vis: q . append(next) vis . add(next) ans += 1 return - 1Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.