Skip to main content
Advanced

Sliding Tile Puzzle — One Move at a Time

Prerequisites and earlier practice
Learn These Concepts Before SolvingStudy highlighted concepts first
→A* Search→Graph BFSComing soon→Hash SetComing soon
1. Problem Prompt & Practice

Read & Attempt the Question First

Given a sliding tile board with numbered tiles and one blank space (0), each move swaps the blank space 4-directionally with an adjacent tile. Return the minimum number of moves needed to reach the ordered goal board, or report if the board configuration is unsolvable.

Input: start = (1, 2, 3, 5, 0, 6, 4, 7, 8) → goal = (1, 2, 3, 4, 5, 6, 7, 8, 0)
Output: 4

Explanation: Slide 5 right, 4 up, 7 left, and 8 left (4 moves total). Counting only numbered tiles' Manhattan distances gives h = 2 at start, never overestimating true moves.

2. BigOCast Visual Solution

BigOCast Studio
More videos on this topic (3)
3. Interactive State Simulator

Interactive 8-Puzzle State-Space & A* Explorer

Click adjacent tiles to slide them manually or step through A* as it evaluates f(board) = g + h across candidate board states!

0

Click any tile adjacent to 0 to slide

Moves g(n)
0
Manhattan h(n)
4
Score f = g + h
4
Inversions
2 (Even)
Initial board: g = 0, h = 4 (numbered tiles 1..8 only), f = 4.
Step 1 / 5 · 5 states popped (out of 181,440 reachable)
4. Step-by-Step Solution & Python Code

Why This Algorithm Solves Sliding Tile Puzzle Optimally

When Each Graph Vertex Is an Entire Board

In grid pathfinding, your position (r, c) is the state. In a Sliding Puzzle (such as the classic 3 × 3 8-Puzzle or 2 × 3 LeetCode 773), moving one tile changes which moves become legal next — so one vertex in the implicit graph is an immutable tuple of the whole board:

START = (1, 2, 3, 5, 0, 6, 4, 7, 8)
GOAL  = (1, 2, 3, 4, 5, 6, 7, 8, 0)

From any board, locating blank = board.index(0) yields up to 4 neighbor tuples, each separated by an edge of weight 1.

Why Counting the Blank Breaks Admissibility

To guide A* with f(board) = g(board) + h(board), we sum how many horizontal + vertical steps each numbered tile (1..8) sits away from its home slot:

  • Each move swaps 0 with one numbered tile, changing that single tile’s Manhattan distance by +1 or −1.
  • Therefore, reducing the numbered-tile Manhattan sum to 0 takes at least h(board) moves (h(board) <= true_remaining_moves).
  • If you accidentally added the blank’s distance too, a single swap could reduce h by 2 in just 1 move — overestimating the remaining cost and potentially returning a non-optimal move sequence!

Complete Python Solution (With Inversion Parity Check)

from heapq import heappop, heappush
from itertools import count

GOAL = (1, 2, 3, 4, 5, 6, 7, 8, 0)


def manhattan(board: tuple[int, ...]) -> int:
    total = 0
    for index, tile in enumerate(board):
        if tile == 0:
            continue
        row, col = divmod(index, 3)
        goal_row, goal_col = divmod(tile - 1, 3)
        total += abs(row - goal_row) + abs(col - goal_col)
    return total


def neighbors(board: tuple[int, ...]):
    blank = board.index(0)
    row, col = divmod(blank, 3)
    for dr, dc in ((-1, 0), (0, -1), (0, 1), (1, 0)):
        nr, nc = row + dr, col + dc
        if 0 <= nr < 3 and 0 <= nc < 3:
            other = 3 * nr + nc
            moved = list(board)
            moved[blank], moved[other] = moved[other], moved[blank]
            yield tuple(moved)


def solvable(board: tuple[int, ...]) -> bool:
    tiles = [t for t in board if t != 0]
    inversions = sum(a > b for i, a in enumerate(tiles) for b in tiles[i + 1:])
    return inversions % 2 == 0


def a_star_puzzle(start: tuple[int, ...]) -> list[tuple[int, ...]] | None:
    if not solvable(start):
        return None
    best = {start: 0}
    parent: dict[tuple[int, ...], tuple[int, ...] | None] = {start: None}
    serial = count()
    h0 = manhattan(start)
    frontier = [(h0, h0, next(serial), 0, start)]

    while frontier:
        _, _, _, cost, board = heappop(frontier)
        if cost != best[board]:
            continue
        if board == GOAL:
            path = []
            curr: tuple[int, ...] | None = board
            while curr is not None:
                path.append(curr)
                curr = parent[curr]
            return path[::-1]

        for nxt in neighbors(board):
            candidate = cost + 1
            if candidate < best.get(nxt, float("inf")):
                best[nxt] = candidate
                parent[nxt] = board
                h = manhattan(nxt)
                heappush(frontier, (candidate + h, h, next(serial), candidate, nxt))

    return None

More Visual Question Walkthroughs

All Questions →