This is a shortest path problem on an implicit graph where each square connects to the next 1 to 6 squares (simulating a dice roll). BFS naturally finds the shortest path in an unweighted graph. The tricky part is converting between square numbers and board coordinates, since the board uses a boustrophedon (zigzag) pattern starting from the bottom-left.
moves + 1.-1 if the final square is unreachable.class Solution:
def snakesAndLadders(self, board: List[List[int]]) -> int:
n = len(board)
def intToPos(square):
r = (square - 1) // n
c = (square - 1) % n
if r % 2 == 1:
c = n - 1 - c
r = n - 1 - r
return r, c
q = deque([(1, 0)])
visit = set()
while q:
square, moves = q.popleft()
for i in range(1, 7):
nextSquare = square + i
r, c = intToPos(nextSquare)
if board[r][c] != -1:
nextSquare = board[r][c]
if nextSquare == n * n:
return moves + 1
if nextSquare not in visit:
visit.add(nextSquare)
q.append((nextSquare, moves + 1))
return -1This variation uses a distance array instead of a visited set, storing the minimum moves to reach each square. This approach is slightly more explicit about tracking distances and allows for early termination once we reach the destination. The core BFS logic remains the same.
-1 for all squares except square 1 (set to 0).bfs from square 1.-1), set its distance and check if it is the destination.-1 if unreachable.class Solution:
def snakesAndLadders(self, board: List[List[int]]) -> int:
n = len(board)
def intToPos(square):
r = (square - 1) // n
c = (square - 1) % n
if r % 2 == 1:
c = n - 1 - c
r = n - 1 - r
return r, c
dist = [-1] * (n * n + 1)
q = deque([1])
dist[1] = 0
while q:
square = q.popleft()
for i in range(1, 7):
nextSquare = square + i
if nextSquare > n * n:
break
r, c = intToPos(nextSquare)
if board[r][c] != -1:
nextSquare = board[r][c]
if dist[nextSquare] == -1:
dist[nextSquare] = dist[square] + 1
if nextSquare == n * n:
return dist[nextSquare]
q.append(nextSquare)
return -1This optimization modifies the board in place to track visited squares, eliminating the need for a separate visited set or distance array. By marking visited positions directly on the board with a special value (0), we reduce memory overhead while maintaining the same BFS traversal logic.
0).0), check for destination and enqueue.0.moves after processing each level and return -1 if unreachable.class Solution:
def snakesAndLadders(self, board: List[List[int]]) -> int:
n = len(board)
def intToPos(square):
r = (square - 1) // n
c = (square - 1) % n
if r % 2 == 1:
c = n - 1 - c
r = n - 1 - r
return r, c
q = deque([1])
board[n - 1][0] = 0
moves = 0
while q:
for _ in range(len(q)):
square = q.popleft()
for i in range(1, 7):
nextSquare = square + i
if nextSquare > n * n:
break
r, c = intToPos(nextSquare)
if board[r][c] != -1:
nextSquare = board[r][c]
if board[r][c] != 0:
if nextSquare == n * n:
return moves + 1
q.append(nextSquare)
board[r][c] = 0
moves += 1
return -1The board uses a boustrophedon (zigzag) pattern starting from the bottom-left, with alternating row directions. A common mistake is not flipping the column index for odd rows or forgetting that row 0 in the board array corresponds to the top of the visual board. Always verify your conversion function with examples from different rows.
After taking a snake or ladder, you land on a different square than where you initially rolled to. The visited check should be based on the final destination after any snake/ladder, but the board position to check for snakes/ladders is the intermediate square. Confusing these leads to revisiting squares or missing valid paths.
When rolling dice from a square close to the end, some rolls may exceed n * n. These rolls are invalid and should be skipped, not treated as reaching the destination. Always check nextSquare > n * n before processing a dice roll.