【ACWing】174. 推箱子

题目地址:

https://www.acwing.com/problem/content/description/176/

推箱子游戏相信大家都不陌生,在本题中,你将控制一个人把 1 1 1个箱子到目的地。给定一张 N N N M M M列的地图,用字符.表示空地,字符#表示墙,字符S表示人的起始位置,字符B表示箱子的起始位置,字符T表示箱子的目标位置。求一种移动方案,使箱子移动的次数最少,在此基础上再让人移动的总步数最少。方案中使用大写的EWSN(东西南北)表示箱子的移动,使用小写的ewsn(东西南北)表示人的移动。
在这里插入图片描述
输入格式:
输入包含多个测试用例。对于每个测试用例,第一行包括两个整数 N , M N, M N,M。接下来 N N N行,每行包括 M M M个字符,用以描绘整个 N N N M M M列的地图。当样例为 N = 0 , M = 0 N=0,M=0 N=0M=0时,表示输入终止,且该样例无需处理。

输出格式:
对于每个测试用例,第一行输出Maze # + + +测试用例的序号。第二行输入一个字符串,表示推箱子的总体移动过程,若无解,则输出Impossible.。每个测试用例输出结束后输出一个空行。若有多条路线满足题目要求,则按照N, S, W, E的顺序优先选择箱子的移动方向(即先上下推,再左右推)。在此前提下,再按照n, s, w, e的顺序优先选择人的移动方向(即先上下动,再左右动)。

数据范围:
1 ≤ N , M ≤ 20 1≤N,M≤20 1N,M20

思路是BFS。考虑如何定义每个状态,除了箱子要计入状态之外,人的位置也要计入状态,因为人的位置会影响下一步箱子能推到哪里。由于要按箱子的最短路优先的次序来搜索答案,所以我们按箱子移动步数一层一层向外扩展,每次扩展的时候,看人是否能走到相应的位置使得箱子从当前状态走到下一个状态。由于路径也要按字典序优先顺序搜索,所以我们搜索的时候,每一步按照上下左右的顺序依次搜索。具体看代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>

using namespace std;
using PII = pair<int, int>;

const int N = 22;
// Node为箱子的状态,也是搜索空间的点,dir表示的是箱子到达(x, y)位置的上一步的朝向下标
struct Node {
    int x, y, dir;
};
int n, m;
char g[N][N];
// 存的是箱子当前状态的上一个状态
Node pre[N][N][4];
// 存的是人的路径中每个位置的上一步的朝向下标
int pre_man[N][N];
// path存的是为了使得箱子到达(j, k, i)这个状态,人所走过的路径的朝向向量,路径是倒着存储的
vector<int> path[N][N][4];
bool vis[N][N][4], vis_man[N][N];
// dist存的是箱子到达(j, k, i)这个状态时,箱子走过的路径长度和人走过的路径长度的pair
PII dist[N][N][4];
// 方向按照上下左右的顺序排列
int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, -1, 1};
char ops[] = "NSWE";

// 判断(x, y)这个位置是否是合法的位置
bool check(int x, int y) {
    return 0 <= x && x < n && 0 <= y && y < m && g[x][y] != '#';
}

// 返回当人在start,箱子在box,并且人要走到end的最少步数,步数相等时取字典序最小的路径,并将路径存入seq中
int bfs_man(PII start, PII end, PII box, vector<int> &seq) {
    memset(vis_man, 0, sizeof vis_man);
    memset(pre_man, -1, sizeof pre_man);

	// 如果起点等于终点则不用走了,步数为0
    if (start == end) return 0;

    queue<PII> q;
    q.push(start);
    // 标记起点为走过
    vis_man[start.first][start.second] = true;
    // 也要标记箱子的位置为走过,因为箱子是不能走到的
    vis_man[box.first][box.second] = true;

    while (q.size()) {
        auto t = q.front(); q.pop();
        for (int i = 0; i < 4; i++) {
        	// 当前位置为(x, y)
            int x = t.first, y = t.second;
            // 尝试走到(a, b),这里搜人下一步的时候要按照上下左右的顺序来搜
            int a = x + dx[i], b = y + dy[i];
            if (check(a, b) && !vis_man[a][b]) {
                vis_man[a][b] = true;
                // 存一下走到(a, b)时最后一步走的朝向
                pre_man[a][b] = i;
                // 如果走到了end,就求一下路径,并返回人的步数 
                if (a == end.first && b == end.second) {
                	// 倒着存路径
                    x = a, y = b;
                    while (pre_man[x][y] != -1) {
                        int dir = pre_man[x][y];
                        seq.push_back(dir);
                        x -= dx[dir], y -= dy[dir];
                    }
                    return seq.size();
                }

                q.push({a, b});
            }
        }
    }

    return -1;
}

bool bfs_box(PII man, PII box, Node &end) {
    memset(vis, 0, sizeof vis);

    queue<Node> q;
    // 以箱子从初始状态移动一步所得da
    for (int i = 0; i < 4; i++) {
        int x = box.first, y = box.second;
        // (a, b)是人要走到的位置,走过去之后,下一步就是推箱子
        int a = x - dx[i], b = y - dy[i];
        // (j, k)是箱子要被推到的位置
        int j = x + dx[i], k = y + dy[i];
        vector<int> seq;
        // 如果人要走到的位置和箱子要被推到的位置都合法,并且人确实能走过去推的话,说明这个状态可以搜到
        if (check(a, b) && check(j, k) && ~bfs_man(man, {a, b}, box, seq)) {
            dist[j][k][i] = {1, seq.size()};
            path[j][k][i] = seq;
            pre[j][k][i] = {x, y, -1};
            // 如果箱子走一步就能到终点,则直接返回
            if (g[j][k] == 'T') {
                end = {j, k, i};
                return true;
            }

            vis[j][k][i] = true;
            q.push({j, k, i});
        }
    }

    bool success = false;
    // 存当前搜到的”最优路径“中箱子移动步数和人移动步数
    PII best_dis = {1e9, 1e9};
    while (q.size()) {
        auto t = q.front(); q.pop();
        for (int i = 0; i < 4; i++) {
        	// 存人走到的位置,下一步要推箱子
            int a = t.x - dx[i], b = t.y - dy[i];
            // 存箱子下一步会被推到的位置
            int j = t.x + dx[i], k = t.y + dy[i];
            // 如果这两个位置都合法,则继续看人能不能走到(a, b)这个位置
            if (check(a, b) && check(j, k)) {
                vector<int> seq;
                // dis存人从当前位置能否走到(a, b)这个位置,如果能,dis存最少步数,否则存-1
                int dis = bfs_man({t.x - dx[t.dir], t.y - dy[t.dir]}, {a, b}, {t.x, t.y}, seq);
                if (~dis) {
                	// 存新状态中箱子的总移动步数和人的总移动步数
                    PII td = {dist[t.x][t.y][t.dir].first + 1, dist[t.x][t.y][t.dir].second + dis};
                    // 如果新状态以前未被访问,或者虽然被访问,但是当前扩展出的方案更优(即人移动的步数更少),则做更新
                    if (!vis[j][k][i] || dist[j][k][i] > td) {
                        path[j][k][i] = seq;
                        pre[j][k][i] = t;
                        dist[j][k][i] = td;
						// 如果新状态恰好就是终点,则标记找到,并且看一下是否是更优方案
                        if (g[j][k] == 'T') {
                            success = true;
                            if (best_dis > dist[j][k][i]) {
                                end = {j, k, i};
                                best_dis = dist[j][k][i];
                            }

                            vis[j][k][i] = true;
                        }

                        if (!vis[j][k][i] && !success) {
                            vis[j][k][i] = true;
                            q.push({j, k, i});
                        }
                    }
                }
            }
        }
    }

    return success;
}

int main() {
    int T = 1;
    while (scanf("%d%d", &n, &m), n || m) {
        printf("Maze #%d\n", T++);

        for (int i = 0; i < n; i++) cin >> g[i];

        PII man, box;
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
                if (g[i][j] == 'S') man = {i, j};
                else if (g[i][j] == 'B') box = {i, j};

        Node end;
        if (!bfs_box(man, box, end)) puts("Impossible.");
        else {
            string res;
            while (!(end.x == box.first && end.y == box.second && end.dir == -1)) {
                res += ops[end.dir];
                for (auto dir: path[end.x][end.y][end.dir]) res += ops[dir] + 32;
                end = pre[end.x][end.y][end.dir];
            }
            reverse(res.begin(), res.end());
            cout << res << endl;
        }
        puts("");
    }

    return 0;
}

时间复杂度 O ( ( N M ) 2 ) O((NM)^2) O((NM)2),空间 O ( N M ) O(NM) O(NM)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
&acute;问题描述: 码头仓库是划分为n×m个格子的矩形阵列。有公共边的格子是相邻格子。当前仓库中 有的格子是空闲的;有的格子则已经堆放了沉重的货物。由于堆放的货物很重,单凭仓库管 理员的力量是无法移动的。仓库管理员有一项任务,要将一个小箱子推到指定的格子上去。 管理员可以在仓库中移动,但不能跨过已经堆放了货物的格子。管理员站在与箱子相对的空 闲格子上时,可以做一次推动,把箱子推到另一相邻的空闲格子。推箱时只能向管理员的对 面方向推。由于要推动的箱子很重,仓库管理员想尽量减少推箱子的次数。 &acute;编程任务: 对于给定的仓库布局,以及仓库管理员在仓库中的位置和箱子的开始位置和目标位置, 设计一个解推箱子问题的分支限界法, 计算出仓库管理员将箱子从开始位置推到目标位置所 需的最少推动次数。 &acute;数据输入: 由文件input.txt提供输入数据。输入文件第 1 行有 2个正整数 n和 m(1<=n,m<=100) , 表示仓库是n×m个格子的矩形阵列。接下来有 n行,每行有 m个字符,表示格子的状态。 S 表示格子上放了不可移动的沉重货物; w 表示格子空闲; M 表示仓库管理员的初始位置; P 表示箱子的初始位置; K 表示箱子的目标位置。 &acute;结果输出: 将计算出的最少推动次数输出到文件 output.txt。如果仓库管理员无法将箱子从开始位 置推到目标位置则输出“No solution!” 。 输入文件示例 输出文件示例 input.txt output.txt
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值