【C/C++】2258. 逃离火灾

485 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情


题目链接:2258. 逃离火灾

题目描述

给你一个下标从 0 开始大小为 m x n 的二维整数数组 grid ,它表示一个网格图。每个格子为下面 3 个值之一:

  • 0 表示草地。
  • 1 表示着火的格子。
  • 2 表示一座墙,你跟火都不能通过这个格子。 一开始你在最左上角的格子 (0, 0) ,你想要到达最右下角的安全屋格子 (m - 1, n - 1) 。每一分钟,你可以移动到 相邻 的草地格子。每次你移动 之后 ,着火的格子会扩散到所有不是墙的 相邻 格子。

请你返回你在初始位置可以停留的 最多 分钟数,且停留完这段时间后你还能安全到达安全屋。如果无法实现,请你返回 -1 。如果不管你在初始位置停留多久,你 总是 能到达安全屋,请你返回 10910^9 。

注意,如果你到达安全屋后,火马上到了安全屋,这视为你能够安全到达安全屋。

如果两个格子有共同边,那么它们为 相邻 格子。

提示:

  • m == grid.length
  • n == grid[i].length
  • 2m,n3002 \leqslant m, n \leqslant 300
  • 4mn21044 \leqslant m * n \leqslant 2 * 10^4
  • grid[i][j] 是 0 ,1 或者 2 
  • grid[0][0] == grid[m - 1][n - 1] == 0

示例 1:

ex1new.jpg

输入: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:

ex2new2.jpg

输入:grid = [[0,0,0,0],[0,1,2,0],[0,2,0,0]]
输出:-1
解释:上图展示了你马上开始朝安全屋移动的情形。
火会蔓延到你可以移动的所有格子,所以无法安全到达安全屋。
所以返回 -1

示例 3:

ex3new.jpg

输入: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
  • 如果无论停留多久都可以到达右下角终点返回 10910^9
  • 如果和火同时到达终点是算能够到达的。

解题思路分析

习惯性动作,首先观察题目数据范围:21042 * 10^4 数据范围较大,暴力遍历停留时间会 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 ,看是否能够到达右下角的终点,最后输出最大可停留时间即可。

具体实现

  1. 火扩散的问题满足 BFS 问题模型,首先对火源进行一次 BFS 扩散,求出火到达每个网格的最短时间 fire[i][j]
  2. 设置二分停留时间的左边界为 -1,表示无法到达右下角终点;右边界为 m * n + 1,表示停留整个网格数量的时间,因为这也是火源覆盖整个图的最大时间。
  3. 二分停留时间 mid,每次设置起点时间为 mid 进行 BFS,看是否能够到达终点:
    • 如果能够到达终点:说明 [l, mid] 时间内都能到达终点。l = mid;
    • 如果不能够到达终点:说明 [mid, r] 时间内都无法到达终点。r = mid;
  4. 最后判断能够到达终点的最大时间 l 即可:
    • -1 :表示无法到达终点,输出 -1
    • m * n + 1 :表示无论停留多久都可以到达终点,此时输出 10910^9
    • 其他情况输出 l 即为最大停留时间。

复杂度分析

  • 时间复杂度:O(mnlog(mn))O(mn\log(mn)),其中 m x n 为网格图的大小,log(mn)log(mn) 为二分所需的时间。
  • 空间复杂度:O(mn)O(mn),其中 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{
    ...
});

结束语

我们很难直到前路有多长,也不一定清楚自己究竟要奔跑多久,但迈开大步向前进、拼尽全力去生活,就是给人生最好的答卷。努力不为感动谁,只为不与最好的自己失之交臂。趁着最美好的年华,继续奋斗吧。