【经典专题】推箱子游戏脚本——一次BFS与多次DFS

题目引入

推箱子 是一款风靡全球的益智小游戏,玩家需要将箱子推到仓库中的目标位置。

游戏地图用大小为 n * m 的网格 grid 表示,其中每个元素可以是墙、地板或者是箱子。

现在你将作为玩家参与游戏,按规则将箱子 ‘B’ 移动到目标位置 ‘T’ :

玩家用字符 ‘S’ 表示,只要他在地板上,就可以在网格中向上、下、左、右四个方向移动。
地板用字符 ‘.’ 表示,意味着可以自由行走。
墙用字符 ‘#’ 表示,意味着障碍物,不能通行。
箱子仅有一个,用字符 ‘B’ 表示。相应地,网格上有一个目标位置 ‘T’。
玩家需要站在箱子旁边,然后沿着箱子的方向进行移动,此时箱子会被移动到相邻的地板单元格。记作一次「推动」。
玩家无法越过箱子。

返回将箱子推到目标位置的最小推动次数,如果无法做到,请返回 -1。

这么熟悉的游戏有读题的必要吗?直接看图:

在这里插入图片描述

 
 

思路讲解

题目不难,静下心来。

试想一下,如果箱子可以自己移动(见鬼),这道题目你会做吗?

这不就变成了一个简单的BFS吗?之所以用BFS而不是DFS,是因为使用BFS(层次化版本)可以一层一层向外扩展,从而轻易得到“最短移动次数”。

再思考,如果加入“箱子需要人从背后推动”的条件,会带来什么不同呢?

箱子的移动受到了限制——只有人可以到达箱子的背后时,箱子才能在这个特定方向进行移动。至于“人是否可以到达箱子的背后”,这个子问题又可以用一次DFS来解决。

由此,我们得到了第一个重要的思路:

以箱子的视角进行BFS(主问题),以人的视角进行DFS(子问题),后者是前者得以进行的前提。

想象一下,此时箱子正位于一个狭窄的“通道”内,这种情况下,人究竟是站在箱子的那一侧就尤为重要。换句话讲,箱子虽然位于同一位置,但人的位置不同,箱子其实仍处于不同的状态(请仔细琢磨“状态”这个用词)。

由此,引出了第二个重要的思路:

箱子的状态包含两个信息,箱子的位置、箱子的来源(它刚刚是以什么样的方向被推来的)。

而我们为什么要纠结于箱子的状态?

因为箱子在BFS时需要设置visited数组来防止重复(实际上防止死循环),而是否发生重复的依据正是箱子的状态。从代码的角度看,我们熟悉的visited数组长这个样子:boolean[][],而现在它变成了这样:boolean[][][4],4是指方向信息。

 
 

细节补充

这部分内容是代码实现上的一些细枝末节,如果你差不多弄懂了上面的思路,完全可以直接跳过此部分去读代码。读完后再来回看这一部分。

细节1

Box类的from属性的含义是“由xxx动作得到”,而不是“由xxx方向得来”。举个例子,[2][3][2]的含义是箱子处于“位置是(2,3),由向左推得来”的状态。

细节2

我们以箱子的视角进行BFS,是不是说人的位置我们就不去跟踪了呢?不是。事实上,人的位置已经与箱子的状态绑定。接着上面的例子,[2][3][2]的含义是箱子处于“位置是(2,3),由向左推得来”的状态,那么对应的人的位置就是(2,2)。

细节3

如何计数走了几步?这就是BFS的经典模板之一,即不一个个出队,而是先记录此时队列中元素的个数(size),然后一次性出队这么多元素,从而得到层次遍历的效果。另外注意本题求的是推动箱子的次数,不是人走的步数。

细节4

人是不能走箱子所在的位置的,这点很容易被忽略。每次一个箱子出队,便立即将该箱子所在的位置置为’B’(不是’.‘的任意字符),处理完这个箱子后,在将该位置改回’.’。

 
 

代码实现

class Solution {

    /**
     *【BFS+DFS】
     * 以箱子的视角进行BFS
     * 以人的视角进行DFS
     * 后者作为前者得以进行的前提
     */
    public int minPushBox(char[][] grid) {
        int m = grid.length;
        int n = grid[0].length;

        // 遍历一次,找出箱子起点/终点,人的初始位置
        int startX = -1;
        int startY = -1;
        int targetX = -1;
        int targetY = -1;
        int personX = -1;
        int personY = -1;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 'B') {
                    startX = i;
                    startY = j;
                }
                if (grid[i][j] == 'T') {
                    targetX = i;
                    targetY = j;
                    grid[i][j] = '.';
                }
                if (grid[i][j] == 'S') {
                    personX = i;
                    personY = j;
                    grid[i][j] = '.';
                }
            }
        }

        // 初始化队列,加入元素以启动BFS
        boolean[][][] visited = new boolean[m][n][4];
        Queue<Box> queue = new LinkedList<>();
        for (int i = 0; i < 4; i++) {
            int[] direction = directions[i];
            if (personCanReach(grid, m, n, personX, personY, startX - direction[0], startY - direction[1], new boolean[m][n])) {
                queue.add(new Box(startX, startY, i));
                visited[startX][startY][i] = true;
            }
        }

        // 以箱子的视角开始BFS
        int step = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            while (size-- > 0) {
                Box box = queue.poll();
                grid[box.x][box.y] = 'B';
                personX = box.x - directions[box.from][0];
                personY = box.y - directions[box.from][1];
                if (box.x == targetX && box.y == targetY) {
                    return step;
                }
                for (int i = 0; i < 4; i++) {
                    int[] direction = directions[i];
                    int nextX = box.x + direction[0];
                    int nextY = box.y + direction[1];
                    // 人是否能绕到箱子的后面?
                    if (!personCanReach(grid, m, n, personX, personY, box.x - direction[0], box.y - direction[1], new boolean[m][n])) {
                        continue;
                    }
                    // 箱子的下个位置是否合法?
                    if (!isValid(grid, m, n, nextX, nextY)) {
                        continue;
                    }
                    // 箱子的下一个状态是不是重复了?
                    if (visited[nextX][nextY][i]) {
                        continue;
                    }
                    queue.add(new Box(nextX, nextY, i));
                    visited[nextX][nextY][i] = true;
                }
                grid[box.x][box.y] = '.';
            }
            step++;
        }
        return -1;
    }

    // 其含义是从【上】【下】【左】【右】
    private final static int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

    // 静态内部类是个顶级类,可当成外部类来看
    private static class Box {
        int x;
        int y;
        int from;
        public Box(int x, int y, int from) {
            this.x = x;
            this.y = y;
            this.from = from;
        }
    }

    // 人是否可以某一位置(startX, startY)到达另一位置(targetX, targetY)
    private boolean personCanReach(char[][] grid, int m, int n, int startX, int startY, int targetX, int targetY, boolean[][] visited) {
        if (startX == targetX && startY == targetY) {
            return true;
        }
        visited[startX][startY] = true;
        for (int[] direction : directions) {
            int nextX = startX + direction[0];
            int nextY = startY + direction[1];
            if (isValid(grid, m, n, nextX, nextY) && !visited[nextX][nextY]) {
                if (personCanReach(grid, m, n, nextX, nextY, targetX, targetY, visited)) {
                    return true;
                }
            }
        }
        return false;
    }

    // 某位置是否可以踏足
    private boolean isValid(char[][] grid, int m, int n, int x, int y) {
        return x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == '.';
    }

}

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

E N D END END

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值