Sliding Puzzle
Problem
Given a 2x3 board representing a sliding puzzle with numbers 1 to 5 and a zero representing the empty space, return the minimum number of moves to reach the target state '123450', or -1 if it is unsolvable.
- board.length == 2
- board[i].length == 3
- 0 ≤ board[i][j] ≤ 5
- All numbers from 0 to 5 appear exactly once
Example
board = [[1,2,3],[4,0,5]]1Starting from '123405' (flattened), the zero is at index 4. It can swap with neighbors at indices 1, 3, or 5. Swapping with index 5 yields '123450', the target state, in one move. The BFS explores states layer by layer, ensuring the first time the target is found is the shortest path.
Approach
Straightforward Solution
A brute-force approach would try all permutations of the board by swapping tiles arbitrarily, which is factorial in complexity and infeasible.
Core Observation
The puzzle states form a graph where each node is a board configuration and edges represent valid moves by sliding the zero tile. The shortest path to the target state corresponds to the minimum number of moves in this graph.
Path to Optimal
Recognizing the problem as a shortest path in an unweighted graph suggests BFS as the optimal approach. BFS explores states in order of increasing moves, guaranteeing the first time the target is reached is the minimal move count. Representing the board as a string simplifies state comparison and hashing. Predefining neighbors for each zero position enables efficient generation of next states.
Optimal Approach
Use BFS starting from the initial board state string. Maintain a queue of (state, moves) and a visited set to avoid revisiting states. For each dequeued state, if it matches the target, return moves. Otherwise, generate all valid next states by swapping zero with its neighbors, enqueue unseen states with moves + 1. If the queue empties without finding the target, return -1.
Time
O(6!)
There are 6! = 720 possible board states. BFS explores each state at most once, so the worst-case time is proportional to the number of states.
Space
O(6!)
The visited set and queue can hold up to all possible states in the worst case, which is factorial in the number of tiles.
Pattern Spotlight
BFS (Shortest Path on Unweighted Graph)
When searching for the minimum number of moves or steps to reach a target configuration in a state space where each move has equal cost, model the problem as a graph and use BFS to guarantee the shortest path is found efficiently.
Solution
| 1 | class Solution: |
| 2 | def slidingPuzzle(self, board: List[List[int]]) -> int: |
| 3 | start = "".join(str(num) for row in board for num in row) |
| 4 | target = "123450" |
| 5 | |
| 6 | neighbors = { |
| 7 | 0: [1,3], |
| 8 | 1: [0,2,4], |
| 9 | 2: [1,5], |
| 10 | 3: [0,4], |
| 11 | 4: [1,3,5], |
| 12 | 5: [2,4] |
| 13 | } |
| 14 | |
| 15 | q = deque([(start, 0)]) |
| 16 | seen = {start} |
| 17 | |
| 18 | while q: |
| 19 | state, moves = q.popleft() |
| 20 | |
| 21 | if state == target: |
| 22 | return moves |
| 23 | |
| 24 | zero = state.index("0") |
| 25 | |
| 26 | for nei in neighbors[zero]: |
| 27 | state_list = list(state) |
| 28 | state_list[zero], state_list[nei] = state_list[nei], state_list[zero] |
| 29 | next_state = "".join(state_list) |
| 30 | |
| 31 | if next_state not in seen: |
| 32 | seen.add(next_state) |
| 33 | q.append((next_state, moves + 1)) |
| 34 | |
| 35 | return -1 |
Step-by-Step Solution
Encode Board State and Define Zero Tile Neighbors
| 3 | start = "".join(str(num) for row in board for num in row) |
| 4 | target = "123450" |
| 6 | neighbors = { |
| 7 | 0: [1,3], |
| 8 | 1: [0,2,4], |
| 9 | 2: [1,5], |
| 10 | 3: [0,4], |
| 11 | 4: [1,3,5], |
| 12 | 5: [2,4] |
| 13 | } |
Objective
To represent the board as a string for easy state comparison and to predefine valid zero tile swap positions for efficient neighbor generation.
Key Insight
Flattening the 2D board into a string creates a unique, hashable representation of each state, enabling quick membership checks in the visited set. Predefining neighbors for each zero index avoids costly computations during BFS, allowing constant-time generation of next states by swapping zero with its neighbors.
Interview Quick-Check
Core Logic
Flattening the board into a string creates a canonical representation of states, enabling O(1) membership checks in the visited set.
Common Pitfalls & Bugs
Forgetting to predefine neighbors leads to repeated, expensive computations of valid moves during BFS.
Perform BFS to Explore States and Track Moves
| 15 | q = deque([(start, 0)]) |
| 16 | seen = {start} |
| 18 | while q: |
| 19 | state, moves = q.popleft() |
| 21 | if state == target: |
| 22 | return moves |
| 24 | zero = state.index("0") |
| 26 | for nei in neighbors[zero]: |
| 27 | state_list = list(state) |
| 28 | state_list[zero], state_list[nei] = state_list[nei], state_list[zero] |
| 29 | next_state = "".join(state_list) |
| 31 | if next_state not in seen: |
| 32 | seen.add(next_state) |
| 33 | q.append((next_state, moves + 1)) |
Objective
To systematically explore all reachable board states using BFS, tracking the number of moves taken to reach each state.
Key Insight
BFS ensures that states are explored in order of increasing moves, so the first time the target state is found, it is guaranteed to be the shortest path. Using a queue and a visited set prevents revisiting states and infinite loops. Generating next states by swapping zero with its neighbors expands the search frontier efficiently.
Interview Quick-Check
Core Logic
BFS explores states layer by layer, guaranteeing the shortest path to the target is found when the target state is dequeued.
State & Boundaries
The loop continues until the queue is empty or the target state is found, ensuring all reachable states are explored.
Common Pitfalls & Bugs
Failing to mark states as visited before enqueueing can cause duplicate states in the queue, leading to inefficiency or infinite loops.
Return -1 if Target State is Unreachable
| 35 | return -1 |
Objective
To indicate failure by returning -1 when the BFS exhausts all states without reaching the target.
Key Insight
If the BFS queue empties without finding the target, it means the target state is unreachable from the initial configuration. Returning -1 signals this impossibility clearly and efficiently.
Interview Quick-Check
State & Boundaries
Returning -1 after BFS completion indicates no solution exists within the state space.
Line Analysis
This solution has 5 Critical lines interviewers watch for.
if state == target:
Check if the current state matches the target state.
Detecting the target state allows immediate termination with the minimal move count.
seen = {start}
Initialize the visited set with the start state to avoid revisits.
Tracking visited states prevents infinite loops and redundant processing in BFS.
return moves
Return the number of moves taken to reach the target state.
Returning here ensures the shortest path length is reported as BFS guarantees minimal moves.
state_list[zero], state_list[nei] = state_list[nei], state_list[zero]
Swap zero with the neighbor tile to create a new state.
Swapping simulates sliding the tile into the empty space, producing a valid next configuration.
return -1
Return -1 if the target state is unreachable after exploring all states.
Exhausting the BFS queue without finding the target confirms no solution exists, so returning -1 signals failure.
start = "".join(str(num) for row in board for num in row)
Flatten the 2D board into a single string representing the start state.
Representing the board as a string creates a unique, hashable state that simplifies comparisons and storage in the visited set.
neighbors = {
Define a dictionary mapping zero tile indices to their valid neighbor indices.
Predefining neighbors enables constant-time lookup of valid moves for the zero tile, avoiding repeated computation during BFS.
q = deque([(start, 0)])
Initialize the BFS queue with the start state and zero moves.
Starting BFS with the initial state sets up the exploration frontier and move count tracking.
state, moves = q.popleft()
Dequeue the next state and its move count for processing.
FIFO ordering guarantees BFS explores states layer by layer, preserving shortest path properties.
for nei in neighbors[zero]:
Iterate over all valid neighbor indices for zero's position.
Exploring neighbors generates all possible next states reachable by one move.
if next_state not in seen:
Check if the next state has not been visited before.
Avoiding revisits prevents cycles and redundant BFS expansions.
seen.add(next_state)
Add the next state to the visited set.
Marking states as visited upon enqueueing ensures each state is processed only once.
q.append((next_state, moves + 1))
Enqueue the next state with incremented move count.
Adding new states to the BFS queue expands the search frontier in order of increasing moves.
target = "123450"
Define the target state string representing the solved puzzle.
Having a fixed target string allows quick equality checks to determine if the puzzle is solved.
while q:
Begin BFS loop to process states until queue is empty.
The loop ensures all reachable states are explored in order of increasing moves.
zero = state.index("0")
Find the index of zero in the current state string.
Locating zero is necessary to determine valid moves by swapping with neighbors.
state_list = list(state)
Convert the current state string into a list for swapping.
Strings are immutable in Python; converting to a list allows swapping tiles to generate new states.
next_state = "".join(state_list)
Convert the swapped list back into a string representing the next state.
Re-encoding the state as a string allows efficient hashing and membership checks.
0: [1,3],
List neighbors for zero at index 0.
Specifying neighbors for each zero position encodes the puzzle's movement constraints precisely.
1: [0,2,4],
List neighbors for zero at index 1.
Each neighbor list reflects the puzzle's adjacency rules, critical for generating valid next states.
2: [1,5],
List neighbors for zero at index 2.
Accurate neighbor mapping ensures BFS explores only valid moves.
3: [0,4],
List neighbors for zero at index 3.
Maintaining correct adjacency is essential to prevent invalid state transitions.
4: [1,3,5],
List neighbors for zero at index 4.
This mapping completes the adjacency graph for the zero tile positions.
5: [2,4]
List neighbors for zero at index 5.
Final neighbor list ensures all zero positions have correct adjacency information.
}
Close the neighbors dictionary.
Completes the data structure used for efficient neighbor lookup.
Test Your Understanding
Why does BFS guarantee the minimum number of moves to reach the target state in this sliding puzzle?
Because BFS explores states in layers of increasing move counts, the first time the target state is dequeued corresponds to the shortest sequence of moves needed to reach it.
Related Problems
BFS pattern
Don't just read it. Drill it.
Reconstruct Sliding Puzzle from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.
Unlock the Sliding Puzzle drill