持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情
题目链接:2258. 逃离火灾
题目描述
给你一个下标从 0 开始大小为 m x n 的二维整数数组 grid ,它表示一个网格图。每个格子为下面 3 个值之一:
0表示草地。1表示着火的格子。2表示一座墙,你跟火都不能通过这个格子。 一开始你在最左上角的格子(0, 0),你想要到达最右下角的安全屋格子(m - 1, n - 1)。每一分钟,你可以移动到 相邻 的草地格子。每次你移动 之后 ,着火的格子会扩散到所有不是墙的 相邻 格子。
请你返回你在初始位置可以停留的 最多 分钟数,且停留完这段时间后你还能安全到达安全屋。如果无法实现,请你返回 -1 。如果不管你在初始位置停留多久,你 总是 能到达安全屋,请你返回 。
注意,如果你到达安全屋后,火马上到了安全屋,这视为你能够安全到达安全屋。
如果两个格子有共同边,那么它们为 相邻 格子。
提示:
m == grid.lengthn == grid[i].lengthgrid[i][j]是0,1或者2grid[0][0] == grid[m - 1][n - 1] == 0
示例 1:
输入:grid = [[0,2,0,0,0,0,0],[0,0,0,2,2,1,0],[0,2,0,0,1,2,0],[0,0,2,2,2,0,2],[0,0,0,0,0,0,0]]
输出:3
解释:上图展示了你在初始位置停留 3 分钟后的情形。
你仍然可以安全到达安全屋。
停留超过 3 分钟会让你无法安全到达安全屋。
示例 2:
输入:grid = [[0,0,0,0],[0,1,2,0],[0,2,0,0]]
输出:-1
解释:上图展示了你马上开始朝安全屋移动的情形。
火会蔓延到你可以移动的所有格子,所以无法安全到达安全屋。
所以返回 -1 。
示例 3:
输入:grid = [[0,0,0],[2,2,0],[1,2,0]]
输出:1000000000
解释:上图展示了初始网格图。
注意,由于火被墙围了起来,所以无论如何你都能安全到达安全屋。
所以返回 10^9 。
整理题意
题目给了大小为 m x n 的网格图,规定左上角 [0, 0] 为起点,右下角 [m - 1, n - 1] 为终点(坐标从 0 开始),网格图中 0 表示草地,1 表示火源,2 表示墙。每一秒钟火源会向旁边除了墙的四周(上、下、左、右)一格进行扩散。问我们最多能在起点停留多少秒钟,还能安全到达右下角的终点。需要注意的是:
- 如果无法到达右下角终点返回
-1; - 如果无论停留多久都可以到达右下角终点返回 ;
- 如果和火同时到达终点是算能够到达的。
解题思路分析
习惯性动作,首先观察题目数据范围: 数据范围较大,暴力遍历停留时间会 TLE 超时,需要考虑优化策略。
遇到图和树的题,往往是需要我们遍历和搜索的,这里首先考虑搜索算法,由于是火蔓延以及最短路,可以想到 BFS 广度优先搜索 算法。
我们考虑到停留的时间具有单调性,换而言之就是如果我们能够在起点停留 t 秒,那么我们就能够在起点停留 [0, t] 秒,如果我们不能在起点停留 t 秒,那我们也肯定不能在起点停留 [t, +∞),满足二分条件,我们首先想到 二分停留时间 t。
那我们可以每次二分停留的时间 t,然后模拟火扩散以及寻找最短路到达终点,判断是否能够到达终点。
对于这里的火扩散模拟每次都是一样的,所以我们可以提前 预处理火扩散到每一个网格的最短时间 来优化时间复杂度:fire[i][j] 表示火扩散到坐标 [i, j] 的最短时间。
由于题目规定和火同时到达终点算能够到达:
- 对于安全屋网格
[n - 1, m - 1]来说,当我们进入[n - 1, m - 1]网格的时间 小于等于fire[n - 1][m - 1]时是可以进入的。 - 对于非安全屋网格来说,当我们进入
[i, j]网格的时间 小于fire[i][j]时是可以进入的。 那我们只需要二分停留的时间,每次对停留的时间进行BFS,看是否能够到达右下角的终点,最后输出最大可停留时间即可。
具体实现
- 火扩散的问题满足
BFS问题模型,首先对火源进行一次BFS扩散,求出火到达每个网格的最短时间fire[i][j]。 - 设置二分停留时间的左边界为
-1,表示无法到达右下角终点;右边界为m * n + 1,表示停留整个网格数量的时间,因为这也是火源覆盖整个图的最大时间。 - 二分停留时间
mid,每次设置起点时间为mid进行BFS,看是否能够到达终点:- 如果能够到达终点:说明
[l, mid]时间内都能到达终点。l = mid; - 如果不能够到达终点:说明
[mid, r]时间内都无法到达终点。r = mid;
- 如果能够到达终点:说明
- 最后判断能够到达终点的最大时间
l即可:-1:表示无法到达终点,输出-1;m * n + 1:表示无论停留多久都可以到达终点,此时输出 ;- 其他情况输出
l即为最大停留时间。
复杂度分析
- 时间复杂度:,其中
m x n为网格图的大小, 为二分所需的时间。 - 空间复杂度:,其中
m x n为网格图的大小,m x n为标记所需的空间大小。
代码实现
代码中使用到了
lambda表达式,这样可以避免了全局变量的使用,增加了代码的可读性。
class Solution {
public:
int maximumMinutes(vector<vector<int>>& grid) {
int dx[4] = {1, 0, -1, 0};
int dy[4] = {0, 1, 0, -1};
int n = grid.size();
int m = grid[0].size();
//fire[i][j] 记录火蔓延到坐标 [i, j]的最短时间
vector<vector<int>> fire(n, vector<int>(m, 1e9));
queue<pair<int, int>> que;
while(que.size()) que.pop();
//将火源放入队列进行蔓延
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
if(grid[i][j] == 1){
fire[i][j] = 0;
que.push(make_pair(i, j));
}
}
}
//先看火的蔓延速度
while(que.size()){
pair<int, int> now = que.front();
que.pop();
for(int i = 0; i < 4; i++){
int nx = now.first + dx[i];
int ny = now.second + dy[i];
int nxtTime = fire[now.first][now.second] + 1;
//lambda表达式(内置check函数,省去全局变量和传参操作)
auto check = [&]()->bool{
if(nx < 0 || nx >= n || ny < 0 || ny >= m) return false;
if(grid[nx][ny] == 1 || grid[nx][ny] == 2) return false;
if(fire[nx][ny] < nxtTime) return false;
return true;
};
//检查下一个点是否可以入队
if(check()){
fire[nx][ny] = nxtTime;
que.push(make_pair(nx, ny));
}
}
}
//二分停留时间
int l = -1, r = m * n + 1;
while(l + 1 != r){
int mid = (l + r) >> 1;
//lambda表达式(内置bfs函数,省去全局变量和传入参数操作)
auto bfs = [&]()->bool{
//创建vis数组记录人的最短到达时间
vector<vector<int>> vis;
vis.resize(n, vector<int>(m, 1e9));
while(que.size()) que.pop();
vis[0][0] = mid;
que.push(make_pair(0, 0));
while(que.size()){
pair<int, int> now = que.front();
que.pop();
if(now.first == n - 1 && now.second == m - 1) return true;
for(int i = 0; i < 4; i++){
int nx = now.first + dx[i];
int ny = now.second + dy[i];
int nxtTime = vis[now.first][now.second] + 1;
auto check = [&]()->bool{
if(nx < 0 || nx >= n || ny < 0 || ny >= m) return false;
if(grid[nx][ny] == 1 || grid[nx][ny] == 2) return false;
//如果遍历过就不用遍历了
if(vis[nx][ny] < nxtTime) return false;
//如果是安全屋,可以等于火到达的时间
if(nx == n - 1 && ny == m - 1){
if(fire[nx][ny] < nxtTime) return false;
}
//非安全屋
else{
if(fire[nx][ny] <= nxtTime) return false;
}
return true;
};
if(check()){
vis[nx][ny] = nxtTime;
if(nx == n - 1 && ny == m - 1) return true;
que.push(make_pair(nx, ny));
}
}
}
return false;
};
if(bfs()) l = mid;
else r = mid;
}
//注意最后判断是否能够到达
return l == m * n ? 1e9 : l;
}
};
总结
本题 核心思想是二分停留时间和 BFS 广度优先搜索,需要注意的是需要提前处理火源扩散到达每个网格的最短时间(第一次 BFS),利用这个最短时间来进行搜索判断能否到达终点(第二次 BFS)。
该题代码中使用到了 lambda 表达式,分别用在了 check() 函数和 bfs() 函数上,这样使用可以避免设置全局变量,简化代码,增加可读性。经常使用操作还有 sort() 函数中的 cmp 自定义比较函数:
//vector<string>& logs
sort(logs.begin(), logs.end(), [&](const string& log1, const string& log2)->bool{
...
});
结束语
我们很难直到前路有多长,也不一定清楚自己究竟要奔跑多久,但迈开大步向前进、拼尽全力去生活,就是给人生最好的答卷。努力不为感动谁,只为不与最好的自己失之交臂。趁着最美好的年华,继续奋斗吧。