Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

909. Snakes and Ladders - Leetcode Solution

Problem Description

The Snakes and Ladders problem asks you to determine the minimum number of moves required to reach the last square on a n x n board, starting from the first square, following these rules:

  • The board is a 2D list, where each cell contains either -1 (an empty square), or a positive integer indicating the destination of a snake or ladder.
  • You start at square 1 (top-left, bottom row) and want to reach square n * n (bottom-right, top row).
  • On each move, you roll a 6-sided die and advance by 1 to 6 squares, unless you land on a square with a snake or ladder, in which case you must move to the destination square indicated in that cell.
  • You must find the minimum number of moves to reach the last square. If it's not possible, return -1.
  • Do not revisit the same square within a path (to avoid cycles).

The board is numbered in a Boustrophedon (zigzag) pattern: left to right on one row, then right to left on the next, and so on, starting from the bottom row.

Thought Process

At first glance, the problem resembles finding the shortest path in a board game, which suggests a graph traversal approach. Each square is a node, and each dice roll represents an edge to another node. However, the challenge lies in the board's zigzag numbering and the presence of snakes and ladders, which can "teleport" you to other squares.

A brute-force approach would try all possible dice rolls from every position, but this quickly becomes infeasible due to the exponential number of possibilities. Instead, we can use Breadth-First Search (BFS) to explore all possible moves level by level, ensuring we find the shortest path first.

The key is to carefully map between the 1D square number and the 2D board coordinates, accounting for the zigzag pattern. We also need to avoid revisiting squares to prevent infinite loops.

Solution Approach

  • Step 1: Map Square Numbers to Board Coordinates
    • Since the board is zigzagged, we need a function to convert a square number (1 to n*n) to its (row, col) on the board.
    • Rows are counted from the bottom (row 0 is the bottom row), and the direction alternates each row.
  • Step 2: BFS Traversal
    • Use a queue to keep track of the current square and the number of moves taken to reach it.
    • For each square, simulate all possible dice rolls (1 to 6).
    • If you land on a square with a ladder or snake (i.e., board cell != -1), move to its destination.
    • Mark squares as visited to avoid cycles.
    • Stop when you reach the last square and return the number of moves.
  • Step 3: Edge Cases
    • If the last square cannot be reached, return -1.

We use a queue for BFS because it guarantees the shortest path is found first, and a visited set or array to prevent revisiting squares.

Example Walkthrough

Suppose we have the following 3x3 board:

  • [ [-1, -1, 9], [-1, -1, -1], [2, -1, -1] ]

Numbering is as follows (bottom-left is 1, top-right is 9):

  • Row 2: 7 8 9
  • Row 1: 6 5 4
  • Row 0: 1 2 3

Let's trace the process:

  1. Start at square 1. Possible moves: 2, 3, 4, 5, 6, 7.
  2. Square 2: has a ladder to 2, so stay at 2.
  3. Square 3: normal.
  4. Square 4: normal.
  5. Square 5: normal.
  6. Square 6: normal.
  7. Square 7: normal.
  8. On the next move, from each reachable square, roll again. If you reach square 9 (which has a ladder to 9 — itself), you finish in 2 moves.

The BFS ensures that the first time you reach square 9 is with the minimum number of moves.

Time and Space Complexity

  • Brute-force: In the worst case, each square can branch into 6 possible moves, leading to O(6^k) time for k moves, which is exponential and infeasible for large boards.
  • Optimized BFS:
    • Each square is visited at most once, so the time complexity is O(n^2), where n is the board width/height.
    • Space complexity is also O(n^2) to store the visited squares and queue.

The BFS approach is efficient and suitable for the problem's constraints.

Summary

The Snakes and Ladders problem is a shortest-path challenge on a special board. By modeling the board as a graph and using BFS, we efficiently find the minimum number of moves to reach the end, while handling the zigzag numbering and snake/ladder jumps. The key insights are mapping between 1D and 2D coordinates and leveraging BFS's level-order traversal to guarantee the shortest path.

Code Implementation

from collections import deque

class Solution:
    def snakesAndLadders(self, board):
        n = len(board)
        
        def get_rc(s):
            quot, rem = divmod(s - 1, n)
            row = n - 1 - quot
            col = rem if quot % 2 == 0 else n - 1 - rem
            return row, col
        
        visited = set()
        queue = deque()
        queue.append((1, 0))  # (square, moves)
        visited.add(1)
        
        while queue:
            s, moves = queue.popleft()
            if s == n * n:
                return moves
            for i in range(1, 7):
                next_s = s + i
                if next_s > n * n:
                    continue
                r, c = get_rc(next_s)
                if board[r][c] != -1:
                    next_s = board[r][c]
                if next_s not in visited:
                    visited.add(next_s)
                    queue.append((next_s, moves + 1))
        return -1
      
#include <vector>
#include <queue>
#include <unordered_set>
using namespace std;

class Solution {
public:
    int snakesAndLadders(vector<vector<int>>& board) {
        int n = board.size();
        auto get_rc = [n](int s) {
            int quot = (s - 1) / n, rem = (s - 1) % n;
            int row = n - 1 - quot;
            int col = (quot % 2 == 0) ? rem : n - 1 - rem;
            return make_pair(row, col);
        };
        vector<bool> visited(n * n + 1, false);
        queue<pair<int, int>> q;
        q.push({1, 0});
        visited[1] = true;
        while (!q.empty()) {
            auto [s, moves] = q.front(); q.pop();
            if (s == n * n) return moves;
            for (int i = 1; i <= 6; ++i) {
                int next_s = s + i;
                if (next_s > n * n) continue;
                auto [r, c] = get_rc(next_s);
                if (board[r][c] != -1) next_s = board[r][c];
                if (!visited[next_s]) {
                    visited[next_s] = true;
                    q.push({next_s, moves + 1});
                }
            }
        }
        return -1;
    }
};
      
import java.util.*;

class Solution {
    public int snakesAndLadders(int[][] board) {
        int n = board.length;
        boolean[] visited = new boolean[n * n + 1];
        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[]{1, 0});
        visited[1] = true;
        
        while (!queue.isEmpty()) {
            int[] curr = queue.poll();
            int s = curr[0], moves = curr[1];
            if (s == n * n) return moves;
            for (int i = 1; i <= 6; ++i) {
                int next_s = s + i;
                if (next_s > n * n) continue;
                int[] rc = getRC(next_s, n);
                int r = rc[0], c = rc[1];
                if (board[r][c] != -1) next_s = board[r][c];
                if (!visited[next_s]) {
                    visited[next_s] = true;
                    queue.offer(new int[]{next_s, moves + 1});
                }
            }
        }
        return -1;
    }
    private int[] getRC(int s, int n) {
        int quot = (s - 1) / n, rem = (s - 1) % n;
        int row = n - 1 - quot;
        int col = (quot % 2 == 0) ? rem : n - 1 - rem;
        return new int[]{row, col};
    }
}
      
/**
 * @param {number[][]} board
 * @return {number}
 */
var snakesAndLadders = function(board) {
    const n = board.length;
    function getRC(s) {
        const quot = Math.floor((s - 1) / n);
        const rem = (s - 1) % n;
        const row = n - 1 - quot;
        const col = (quot % 2 === 0) ? rem : n - 1 - rem;
        return [row, col];
    }
    const visited = new Array(n * n + 1).fill(false);
    const queue = [];
    queue.push([1, 0]);
    visited[1] = true;
    while (queue.length > 0) {
        const [s, moves] = queue.shift();
        if (s === n * n) return moves;
        for (let i = 1; i <= 6; ++i) {
            let next_s = s + i;
            if (next_s > n * n) continue;
            const [r, c] = getRC(next_s);
            if (board[r][c] !== -1) next_s = board[r][c];
            if (!visited[next_s]) {
                visited[next_s] = true;
                queue.push([next_s, moves + 1]);
            }
        }
    }
    return -1;
};