题目描述

✅ 1263. 推箱子

image-20260928230715947

image-20260928230715948

image-20260928230715949

题意分析

玩家可以在空地上四向行走,但不能穿过墙或当前箱子。只有站在箱子后方,才能把它向前推一格;求送到目标位置所需的最少推动次数,无法送达时返回 -1。

人绕行多少步不计入答案,所以不能把走路和推箱子都当成一次普通 BFS 移动。这里把任意次自由行走压缩成一次可达性检查,让主搜索只记录推动。

解法:按推箱子次数分层 BFS

核心思路

[!blue]

主状态是 (boxPos, playerPos),两者都用 row * cols + col 编码。只记录箱子位置不够:箱子相同但人位于不同区域时,能够推动的方向可能不同。

若要沿方向 (dr, dc) 推动,箱子落点是 (boxRow + dr, boxCol + dc),人的站位是 (boxRow - dr, boxCol - dc)。两处都必须在地图内且不是墙;此外,还要确认人能从当前位置走到站位,不能只看站位本身是不是空地。

为此再做一次普通 BFS,把当前箱子位置视为额外障碍,其余非墙格都可走。这里只回答是否可达,不把走路步数加入答案。地图中的 B 是最初的标记,箱子移动后应阻挡新的位置,旧 B 格可以再次经过。

推动成功后,箱子进入落点,人进入箱子的旧位置,新状态固定为 (nextBox, boxPos)。每条主搜索边都恰好增加一次推动,因此按层 BFS,首次取出箱子到达目标的状态时,层数就是最少推动次数。

用 visited[boxPos][playerPos] 在入队时去重。相同状态之后的可推动方向完全相同,而 BFS 首次到达它的推动数最少,之后再到达无需重复展开。主队列耗尽仍未到目标,就说明不存在可行方案。

解题步骤

  1. 扫描地图找到 B、S、T,将初始双位置状态入队并标记,初始化 pushes = 0。
  2. 每轮先记录当前队列长度,只处理这一层的状态;若箱子已在目标上,返回 pushes。
  3. 枚举四个推动方向,排除落点或站位不可走、结果状态已经访问的情况。
  4. 固定箱子不动,检查玩家能否到达所需站位;可达才把新状态标记并入队。
  5. 当前层处理完后让 pushes 加一。全部状态搜索完仍未到达目标则返回 -1。

代码实现

class Solution {
    private static final int[][] DIRECTIONS = {
        {1, 0},
        {-1, 0},
        {0, 1},
        {0, -1}
    };

    private int rows;
    private int cols;

    public int minPushBox(char[][] grid) {
        rows = grid.length;
        cols = grid[0].length;
        int total = rows * cols;
        int box = -1;
        int player = -1;
        int target = -1;

        for (int row = 0; row < rows; row++) {
            for (int col = 0; col < cols; col++) {
                int pos = row * cols + col;

                if (grid[row][col] == 'B') {
                    box = pos;
                } else if (grid[row][col] == 'S') {
                    player = pos;
                } else if (grid[row][col] == 'T') {
                    target = pos;
                }
            }
        }

        boolean[][] visited = new boolean[total][total];
        Deque<int[]> queue = new ArrayDeque<>();

        queue.offer(new int[] {
            box,
            player
        });
        visited[box][player] = true;

        int pushes = 0;

        while (!queue.isEmpty()) {
            int size = queue.size();

            for (int i = 0; i < size; i++) {
                int[] state = queue.poll();
                int boxPos = state[0];
                int playerPos = state[1];

                if (boxPos == target) {
                    return pushes;
                }

                int boxRow = boxPos / cols;
                int boxCol = boxPos % cols;

                for (int[] direction : DIRECTIONS) {
                    int nextBoxRow = boxRow + direction[0];
                    int nextBoxCol = boxCol + direction[1];
                    // 人必须站在箱子移动方向的反侧。
                    int standRow = boxRow - direction[0];
                    int standCol = boxCol - direction[1];

                    if (!isOpen(grid, nextBoxRow, nextBoxCol)
                            || !isOpen(grid, standRow, standCol)) {
                        continue;
                    }

                    int nextBox = nextBoxRow * cols + nextBoxCol;
                    int stand = standRow * cols + standCol;

                    if (visited[nextBox][boxPos]) {
                        continue;
                    }

                    // 把当前箱子视为障碍,检查人能否绕到推动站位。
                    if (!canReach(grid, playerPos, stand, boxPos)) {
                        continue;
                    }

                    visited[nextBox][boxPos] = true;
                    // 推完后人位于箱子原位置,主搜索只计这一次推动。
                    queue.offer(new int[] {
                        nextBox,
                        boxPos
                    });
                }
            }

            pushes++;
        }

        return -1;
    }

    private boolean canReach(char[][] grid, int start, int target, int box) {
        boolean[] visited = new boolean[rows * cols];
        Deque<Integer> queue = new ArrayDeque<>();

        queue.offer(start);
        visited[start] = true;

        while (!queue.isEmpty()) {
            int pos = queue.poll();

            if (pos == target) {
                return true;
            }

            int row = pos / cols;
            int col = pos % cols;

            for (int[] direction : DIRECTIONS) {
                int nextRow = row + direction[0];
                int nextCol = col + direction[1];

                if (!isOpen(grid, nextRow, nextCol)) {
                    continue;
                }

                int next = nextRow * cols + nextCol;

                // 人的自由移动不能穿过当前箱子。
                if (next == box || visited[next]) {
                    continue;
                }

                visited[next] = true;
                queue.offer(next);
            }
        }

        return false;
    }

    private boolean isOpen(char[][] grid, int row, int col) {
        return row >= 0 && row < rows && col >= 0 && col < cols && grid[row][col] != '#';
    }
}
var pushBoxDirections = [][2]int{
    {1, 0},
    {-1, 0},
    {0, 1},
    {0, -1},
}

func minPushBox(grid [][]byte) int {
    rows := len(grid)
    cols := len(grid[0])
    total := rows * cols
    box := -1
    player := -1
    target := -1

    for row := 0; row < rows; row++ {
        for col := 0; col < cols; col++ {
            pos := row*cols + col
            if grid[row][col] == 'B' {
                box = pos
            } else if grid[row][col] == 'S' {
                player = pos
            } else if grid[row][col] == 'T' {
                target = pos
            }
        }
    }

    visited := make([][]bool, total)
    for i := 0; i < total; i++ {
        visited[i] = make([]bool, total)
    }

    queue := [][2]int{
        {box, player},
    }
    visited[box][player] = true
    pushes := 0
    for len(queue) > 0 {
        size := len(queue)
        for i := 0; i < size; i++ {
            state := queue[i]
            boxPos := state[0]
            playerPos := state[1]
            if boxPos == target {
                return pushes
            }

            boxRow := boxPos / cols
            boxCol := boxPos % cols
            for _, direction := range pushBoxDirections {
                nextBoxRow := boxRow + direction[0]
                nextBoxCol := boxCol + direction[1]
                // 人必须站在箱子移动方向的反侧。
                standRow := boxRow - direction[0]
                standCol := boxCol - direction[1]

                if !isPushBoxOpen(grid, nextBoxRow, nextBoxCol) || !isPushBoxOpen(grid, standRow, standCol) {
                    continue
                }

                nextBox := nextBoxRow*cols + nextBoxCol
                stand := standRow*cols + standCol
                if visited[nextBox][boxPos] {
                    continue
                }

                // 把当前箱子视为障碍,检查人能否绕到推动站位。
                if !canPushBoxReach(grid, playerPos, stand, boxPos) {
                    continue
                }

                visited[nextBox][boxPos] = true
                // 推完后人位于箱子原位置,主搜索只计这一次推动。
                queue = append(queue, [2]int{
                    nextBox,
                    boxPos,
                })
            }
        }
        queue = queue[size:]
        pushes++
    }

    return -1
}

func canPushBoxReach(grid [][]byte, start int, target int, box int) bool {
    rows := len(grid)
    cols := len(grid[0])
    visited := make([]bool, rows*cols)
    queue := []int{
        start,
    }
    visited[start] = true

    for head := 0; head < len(queue); head++ {
        pos := queue[head]
        if pos == target {
            return true
        }

        row := pos / cols
        col := pos % cols
        for _, direction := range pushBoxDirections {
            nextRow := row + direction[0]
            nextCol := col + direction[1]
            if !isPushBoxOpen(grid, nextRow, nextCol) {
                continue
            }

            next := nextRow*cols + nextCol
            // 人的自由移动不能穿过当前箱子。
            if next == box || visited[next] {
                continue
            }

            visited[next] = true
            queue = append(queue, next)
        }
    }

    return false
}

func isPushBoxOpen(grid [][]byte, row int, col int) bool {
    return row >= 0 && row < len(grid) && col >= 0 && col < len(grid[0]) && grid[row][col] != '#'
}

复杂度分析

  • 时间复杂度:$O(V^2)$,其中 V 为格子数。除初始状态外,每次推动后人都在箱子四邻之一,所以实际主状态至多为 4V + 1;每个状态最多进行四次 $O(V)$ 的站位检查。初始化完整访问表也需要 $O(V^2)$。
  • 空间复杂度:$O(V^2)$,当前实现分配箱子位置乘人位置的完整访问表。

关键点总结

[!green]

  • 推完后人的位置是箱子旧位置,不是原来的远处位置。
  • 当前箱子阻挡人行走,原来的 B 标记格并非永久障碍。

易错点总结

[!yellow]

  • 只按箱子位置去重,会忽略人处于不同可达区域。
  • 站位和落点放在同一侧,会变成拉箱子。
  • 人的普通走动也计一层,会求成总动作最少而非推动最少。

相似题目

题目 难度 关联与区别
1368. 使网格图至少有一条有效路径的最小代价 困难 步行可视为0费用、推箱为1费用,和原题的0/1边权类似,可用0-1 BFS组织搜索。
773. 滑动谜题 困难 同样搜索棋盘状态,本题需同时记录玩家和箱子位置,且优化推箱次数而不是总步数。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2025/13603620
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!