题目链接
Leetcode.1263 推箱子
rating : 2297
题目描述
「推箱子」是一款风靡全球的益智小游戏,玩家需要将箱子推到仓库中的目标位置。
游戏地图用大小为 m × n m \times n m×n 的网格 g r i d grid grid 表示,其中每个元素可以是墙、地板或者是箱子。
现在你将作为玩家参与游戏,按规则将箱子 'B'
移动到目标位置 'T'
:
- 玩家用字符
'S'
表示,只要他在地板上,就可以在网格中向上、下、左、右四个方向移动。 - 地板用字符
'.'
表示,意味着可以自由行走。 - 墙用字符
'#'
表示,意味着障碍物,不能通行。 - 箱子仅有一个,用字符
'B'
表示。相应地,网格上有一个目标位置'T'
。 - 玩家需要站在箱子旁边,然后沿着箱子的方向进行移动,此时箱子会被移动到相邻的地板单元格。记作一次「推动」。
- 玩家无法越过箱子。
返回将箱子推到目标位置的最小 推动 次数,如果无法做到,请返回 − 1 -1 −1。
示例 1:
输入:grid = [[“#”,“#”,“#”,“#”,“#”,“#”],
[“#”,“T”,“#”,“#”,“#”,“#”],
[“#”,“.”,“.”,“B”,“.”,“#”],
[“#”,“.”,“#”,“#”,“.”,“#”],
[“#”,“.”,“.”,“.”,“S”,“#”],
[“#”,“#”,“#”,“#”,“#”,“#”]]
输出:3
解释:我们只需要返回推箱子的次数。
示例 2:
输入:grid = [[“#”,“#”,“#”,“#”,“#”,“#”],
[“#”,“T”,“#”,“#”,“#”,“#”],
[“#”,“.”,“.”,“B”,“.”,“#”],
[“#”,“#”,“#”,“#”,“.”,“#”],
[“#”,“.”,“.”,“.”,“S”,“#”],
[“#”,“#”,“#”,“#”,“#”,“#”]]
输出:-1
示例 3:
输入:grid = [[“#”,“#”,“#”,“#”,“#”,“#”],
[“#”,“T”,“.”,“.”,“#”,“#”],
[“#”,“.”,“#”,“B”,“.”,“#”],
[“#”,“.”,“.”,“.”,“.”,“#”],
[“#”,“.”,“.”,“.”,“S”,“#”],
[“#”,“#”,“#”,“#”,“#”,“#”]]
输出:5
解释:向下、向左、向左、向上再向上。
提示:
- m = g r i d . l e n g t h m = grid.length m=grid.length
- n = g r i d [ i ] . l e n g t h n = grid[i].length n=grid[i].length
- 1 ≤ m , n ≤ 20 1 \leq m, n \leq 20 1≤m,n≤20
-
g
r
i
d
grid
grid 仅包含字符
'.', '#', 'S' , 'T', 以及 'B'
。 -
g
r
i
d
grid
grid 中
'S', 'B' 和 'T'
各只能出现一个。
解法:01 bfs
我们把 (箱子的位置,玩家的位置) 看成是一个整体的状态。
我们记 m = g r i d . s i z e ( ) , n = g r i d [ 0 ] . s i z e ( ) m = grid.size() , n = grid[0].size() m=grid.size(),n=grid[0].size(),箱子的位置是 ( b x , b y ) (bx,by) (bx,by),玩家的位置是 ( s x , s y ) (sx,sy) (sx,sy)。
我们定义
f
(
箱子的位置,玩家的位置
)
f(箱子的位置,玩家的位置)
f(箱子的位置,玩家的位置) 为处于这个状态的最小操作数,那么我们最终的答案就是
f
(
′
T
′
的位置,玩家的位置
)
f('T' 的位置,玩家的位置)
f(′T′的位置,玩家的位置),因为我们只需要把 箱子推到终点 'T'
即可,这样看来 终点可能有多个,我们只需要求距离起点路径和最小的那一个即可。(将整体看成是遍历起点到终点的一张图)
为了压缩状态,我们可以定义箱子的位置为 : b = b x × n + b y b = bx \times n + by b=bx×n+by;定义玩家的位置为 : s = s x × n + s y s = sx \times n + sy s=sx×n+sy,即 f ( b , s ) f(b,s) f(b,s);
起点就是 初始箱子的位置 也就是 'B'
所在的位置
(
b
x
,
b
y
)
(bx,by)
(bx,by),初始玩家的位置 也就是 'S'
所在的位置
(
s
x
,
s
y
)
(sx,sy)
(sx,sy)。即
f
(
b
x
×
n
+
b
y
,
s
x
×
n
+
s
y
)
f(bx \times n + by , sx \times n + sy)
f(bx×n+by,sx×n+sy)就是起点的状态,起点状态初始为
0
0
0;
对于 推箱子 的操作,我们才让操作数 + 1 +1 +1;只移动 玩家位置 的操作,我们只更新状态。
进行 01 bfs。
首先出队,如果当前状态的 箱子位置就是终点位置,那么就直接返回 当前状态的最小操作数。
我们先让 玩家 向 上下左右四个方向移动。
如果 玩家 的新位置是合法的,并且移到了箱子所在的位置,那么箱子就要被推走(因为箱子的位置已经被占了),此时如果状态合法的话,就将 当前状态更新为 = 旧状态 + 1 当前状态更新为 = 旧状态 + 1 当前状态更新为=旧状态+1,将 当前状态加入到队尾。
如果 玩家 的新位置是合法的,没有 移动到箱子所在的位置,那么就只更新状态,将 当前状态加入到队头。
这样做的原因是:
在计算 推了箱子后的状态 之前,必须保证之前 没推箱子的状态 已经全部计算完成,不然可能会出现一种情况,就是箱子推了一圈回到原来位置,但是由于没有把之前的状态全部计算出来,就会误认为绕了一圈的推动次数比什么都不做的推动次数要少,造成最终结果比实际结果偏大。所以前者状态要放在队头,后者状态要放在队尾。
时间复杂度: O ( m 2 × n 2 ) O(m^2 \times n^2) O(m2×n2)
C++代码:
using PII = pair<int,int>;
const int dx[4] = {1,0,-1,0};
const int dy[4] = {0,1,0,-1};
class Solution {
public:
int minPushBox(vector<vector<char>>& grid) {
int m = grid.size() , n = grid[0].size();
int sx = 0,sy = 0,bx = 0,by = 0;
for(int i = 0;i < m;i++){
for(int j = 0;j < n;j++){
if(grid[i][j] == 'S'){
sx = i , sy = j;
}
else if(grid[i][j] == 'B'){
bx = i , by = j;
}
}
}
auto check = [&](int i,int j){
if(i < 0 || i >= m || j < 0 || j >= n || grid[i][j] == '#') return false;
return true;
};
vector<vector<int>> f(m*n , vector<int> (m*n , 1e9));
deque<PII> q;
q.emplace_back(bx * n + by , sx * n + sy);
f[bx * n + by][sx * n + sy] = 0;
while(!q.empty()){
auto [b1,s1] = q.front();
q.pop_front();
int sx1 = s1 / n , sy1 = s1 % n , bx1 = b1 / n , by1 = b1 % n;
//箱子已经被推到终点
if(grid[bx1][by1] == 'T') return f[b1][s1];
for(int k = 0;k < 4;k++){
//玩家移动
int sx2 = sx1 + dx[k] , sy2 = sy1 + dy[k] , s2 = sx2 * n + sy2;
//判断玩家移动位置是否合法
if(!check(sx2,sy2)) continue;
//如果此时玩家的位置恰好在 箱子的位置上,说明箱子被要被推动
//此时可以推动箱子
if(sx2 == bx1 && sy2 == by1){
int bx2 = bx1 + dx[k] , by2 = by1 + dy[k] , b2 = bx2 * n + by2;
//判断移动箱子是否合法
if(!check(bx2 , by2) || f[b2][s2] <= f[b1][s1] + 1) continue;
f[b2][s2] = f[b1][s1] + 1;
q.emplace_back(b2,s2);
}
//不能推动箱子,只是移动了玩家位置
else{
if(f[b1][s2] <= f[b1][s1]) continue;
f[b1][s2] = f[b1][s1];
q.emplace_front(b1,s2);
}
}
}
return -1;
}
};