推箱子游戏解法的数学分析与代码实现(Leetcode1263等未完)

推箱子游戏是将箱子推到目标点,是一个经典的游戏,这个问题存在一个最优解,即最短走法,这里通过分析一个箱子与多个箱子,探讨问题的解法。

一个箱子问题链接:leetcode推箱子
这个问题是一个阉割版,不考虑人走的步数,只考虑箱子被推到目标位置(前提是人不被卡住)
思路
考虑使用BFS,计算每个点离目标点的可推最短距离(自定义距离,可以推动的最短距离)
为了简化计算,还要用一个数组记录已经遍历过的点。 计算箱子所在当前点与目标点的距离,从初始点向四个方向扩展,直到找到目标点(反过来计算也可以),为了避免找到的不是最优解,可以在每个点加入队列时按照移动步数steps进行排序。
代码

class Solution {
    public int minPushBox(char[][] grid) {

        int rlen = grid.length;
        int llen = grid[0].length;
        //visited[i][j][m][n] = true 表示 人物在 (i, j) 坐标和 箱子在 (m, n) 坐标 这个状态已经访问过了
        boolean[][][][] visited = new boolean[rlen][llen][rlen][llen];
        int sx = -1, sy = -1, tx = -1, ty = -1, bx = -1, by = -1;
        int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
        for (int i = 0; i < rlen; i++) {
            for (int j = 0; j < llen; j++) {
                if (grid[i][j] == 'S') {
                    sx = i;
                    sy = j;
                }
                if (grid[i][j] == 'T') {
                    tx = i;
                    ty = j;
                }
                if (grid[i][j] == 'B') {
                    bx = i;
                    by = j;
                }
            }
        }

        /*
        当人物在箱子的左边时,人物可以选择向右边走
        当人物在箱子的右边时,人物可以选择向左边走
        当人物在箱子的上边时,人物可以选择向下边走
        当人物在箱子的下边时,人物可以选择向上边走
        这样才能保证步数最少,否则,如果箱子在左边,人物还向着右边走,那么就距离箱子越来越远,这是毫无意义的步数

        无法满足条件的情况:
        如果箱子会自己走的话,那么简单的 bfs 就能够完成了,但是这里需要人物来推动箱子
        这意味着箱子可能虽然旁边就是终点,但是可能不存在能够容纳人物的地方来推动箱子
        比如 下图,虽然 箱子 B 旁边就是终点 T,如果它能够自己走的话直接一步到终点
        但是由于需要推动,而却不存在容纳人物 S 的位置来 箱子 B 到达终点 T
        # # # #
        # T B #
        # . S #

        什么时候箱子的位置会发生改变?
        当人物向上下两个方向走的时候,如果人物的下一个位置就是箱子的位置,
        那么相当于顶着箱子前进,那么箱子同时也要往前进

        因为人物的移动不算在步数里的,因此可能移动的时候当前箱子推的步数很大,比如示例 1 ,人物绕了一大圈
        如果最优的情况,即最少的推箱子步数就是这绕一大圈的,但是别的状态还在推箱子,它能够更快的到达终点,但是推箱子步数很大
        所以最先碰到终点的不一定是步数最少的,所以需要使用一个优先队列
        */
        PriorityQueue<Node> queue = new PriorityQueue<>((a, b) -> a.steps - b.steps);  // 按steps进行升序排列,确保找到的一定是最短的
        queue.add(new Node(0, sx, sy, bx, by));    // 初始点


        while (!queue.isEmpty()) {
            Node t = queue.poll();

            int currentBoxX = t.bx, currentBoxY = t.by;
            if (currentBoxX == tx && currentBoxY == ty) {     // 箱子到了目标点
                return t.steps;
            }
            visited[t.hx][t.hy][currentBoxX][currentBoxY] = true;

            //往四个方向走
            for (int[] p : directions) {
                int newPx = t.hx + p[0];
                int newPy = t.hy + p[1];
                int newBx = t.bx;
                int newBy = t.by;
                int newStep = t.steps;
                //人物的前进位置刚好是箱子的位置,那么箱子的位置也要发生改变
                if (newPx == t.bx && newPy == t.by) {
                    newBx += p[0];
                    newBy += p[1];
                    //箱子动了,步数 +1
                    newStep++;
                }
                //越界或者在障碍物上,那么跳过
                if (!legalIndex(newBx, rlen, newBy, llen) || !legalIndex(newPx, rlen, newPy, llen)
                        || grid[newPx][newPy] == '#' || grid[newBx][newBy] == '#') {
                    continue;
                }
                if (!visited[newPx][newPy][newBx][newBy]) {
                    queue.add(new Node(newStep, newPx, newPy, newBx, newBy));
                }
            }


        }
        return -1;

    }

    private static boolean legalIndex(int x, int rlen, int y, int llen) {
        return x >= 0 && x < rlen && y >= 0 && y < llen;
    }


    static class Node {
        int steps;
        int hx;
        int hy;
        int bx;
        int by;

        public Node(int steps, int hx, int hy, int bx, int by) {
            this.steps = steps;
            this.hx = hx;
            this.hy = hy;
            this.bx = bx;
            this.by = by;
        }
    }
}

然而,这个解法不够智能,只击败了10%的java提交,我们有以下方法可以进行优化

使用并查集进行优化

class Solution {
    //1. 并查集判断能否到达
    //2. BFS 箱子
    int[][] dir = new int[][]{{-1, 0}, {1, 0}, {0, 1}, {0, -1}};
    public int minPushBox(char[][] grid) {
        int m = grid.length, n = grid[0].length;
        int px = 0, py = 0, tx = 0, ty = 0, bx = 0, by = 0;
        for(int i = 0; i < m; i++) {
            for(int j = 0; j < n; j++) {
                if(grid[i][j] == 'B') {
                    bx = i;
                    by = j;
                } else if(grid[i][j] == 'S') {
                    px = i;
                    py = j;
                } else if(grid[i][j] == 'T') {
                    tx = i;
                    ty = j;
                }
            }
        }
        int[] parent = buildSet(grid, m, n);
        int sp = find(parent, px * n + py);
        int st = find(parent, tx * n + ty);
        int sb = find(parent, bx * n + by);
        if(!(sp == st && st == sb)) return -1;
        
        Queue<int[]> que = new LinkedList<>();
        Set<String> visited = new HashSet<>();
        que.offer(new int[]{bx * n + by, px * n + py});
        visited.add((bx * n + by) + "," + (px * n + py));
        
        int res = 0;
        while(!que.isEmpty()) {
            for(int size = que.size(); size > 0; size--) {
                int[] p = que.poll();
                if(p[0] == tx * n + ty) return res;
                int r = p[0] / n, c = p[0] % n;
                
                char ch = grid[r][c];
                grid[r][c] = '#';
                parent = buildSet(grid, m, n);
                for(int[] d : dir) {
                    int x = r + d[0], y = c + d[1];
                    int rx = r - d[0], ry = c - d[1];
                    
                    if(x >= 0 && x < m && y >= 0 && y < n && grid[x][y] != '#' && rx >= 0 && rx < m && ry >= 0 && ry < n && grid[rx][ry] != '#') {
                        
                        if(find(parent, rx * n + ry) == find(parent, p[1]) && visited.add((x * n + y) + "," + p[0])) {
                            que.offer(new int[]{x * n + y, p[0]});
                        }
                    }
                }
                grid[r][c] = (char)(res + '0');
            }
            res++;
        }
        return -1;
    }
    
    private int[] buildSet(char[][] grid, int m, int n) {
        int[] parent = new int[m * n];
        for(int i = 0; i < m * n; i++) parent[i] = i;
        for(int i = 0; i < m; i ++) {
            for(int j = 0; j < n; j++) {
                if(grid[i][j] != '#') {
                    if(i > 0 && grid[i-1][j] != '#') union(parent, (i-1)*n+j, i*n+j);
                    if(i < m-1 && grid[i+1][j] != '#') union(parent, (i+1)*n+j, i*n+j);
                    if(j > 0 && grid[i][j-1] != '#') union(parent, i*n+j-1, i*n+j);
                    if(j < n-1 && grid[i][j+1] != '#') union(parent, i*n+j+1, i*n+j);
                }
            }
        }
        return parent;
    }
    
    private void union(int[] p, int a, int b) {
        int pa = find(p, a), pb = find(p, b);
        if(pa != pb) p[pa] = pb;
    }
    
    private int find(int[] p, int a) {
        return p[a] = p[a] == a ? a : find(p, p[a]);
    }
}

击败了41%的java提交

更好的解法

可以用启发式搜索对这个寻找过程进行加速。
箱子用了A*,估价函数f返回值里,第一比较值是取曼哈顿距离和已走步数之和,保证在箱体内搜索出步数最优值,后续比较值是box的坐标。
人用best-first算法,原理和A*相似,只是不用考虑最短步数,只优先考虑距离终点近点的扩展。
因为复数不能在堆里直接比较,所以加了time和pTime计数器保证优先处理先进堆的元素,且不会比较到复数坐标。

A*算法和BF算法单轮无障碍情况下不大于
O((M+N)log(M+N))的时间,对数计算来自于堆的复杂度,单轮有障碍则是
O(MNlog(MN)),所以总的最坏时间复杂度则是
O(M 2 N 2 log(MN) 2),考虑三个关键点的大多分布在地图周围,但人的行走步数拓展层次多数情况下不会很深,且由于估价函数的存在,大大提升了寻路效率,线上测试平均优化掉一半时间是符合逻辑的。

代码:

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值