2258. 逃离火灾 | BFS | 二分答案

32 阅读3分钟

题目描述:

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

  • 0 表示草地。
  • 1 表示着火的格子。
  • 2 表示一座墙,你跟火都不能通过这个格子。

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

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

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

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

前提:如果人可以在初始位置停留 3 分钟,那同样也可以在初始位置停留 0,1,2 分钟;人不能在初始位置停留 4 分钟,那同样也不能在初始位置停留 5,6,7,⋯ 分钟。因此可以考虑使用二分答案的做法,即假设答案为 t,判断是否可行。

判断函数 check 的逻辑:

  1. 用一个二维数据 onFire 记录当前哪些位置已经着火;
  2. 人现在原地停留 t 分钟,因此通过 BFS 让火扩散 t 分钟,此时如果烧到了起点,证明 check 失败,要减小 t
  3. 然后每分钟人先移动,火再移动。如果下一分钟人的格子出队时,发现已经被火烧到了,则直接跳过。如果人可以到达安全屋,则说明 check 成功,要增大 t
class Solution {
    private static final int[][] DIRS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

    private boolean check(int[][] grid, int min) {
        int m = grid.length, n = grid[0].length;
        // 用二维数组表示现在的火势
        boolean[][] onFire = new boolean[m][n];
        // 用数组来模拟 BFS,和队列貌似区别不大
        List<int[]> f = new ArrayList<>();
        for(int i = 0; i < m; i++) {
            for(int j = 0; j < n; j++) {
                if(grid[i][j] == 1) {
                    onFire[i][j] = true;
                    f.add(new int[]{i, j});
                }
            }
        }
        // 火先扩散 min 分钟
        while(min-- > 0 && !f.isEmpty()) {
            f = spreadFire(grid, onFire, f);
        }
        // min 分钟后如果火势扩散到了起点直接返回失败
        if(onFire[0][0]) return false;

        List<int[]> p = new ArrayList<>();
        boolean[][] vis = new boolean[m][n];
        p.add(new int[]{0, 0});
        vis[0][0] = true;
        // 经典的 BFS 写法
        while(!p.isEmpty()) {
            List<int[]> tmp = p;
            p = new ArrayList<>();
            for(int[] pos : tmp) {
                int x = pos[0], y = pos[1];
                // 当前位置已经被火烧了
                if(onFire[x][y]) continue;
                for(int[] d : DIRS) {
                    int nx = x + d[0], ny = y + d[1];
                    if(nx >= 0 && nx < m && ny >= 0 && ny < n && !onFire[nx][ny] && !vis[nx][ny] && grid[nx][ny] == 0) {
                        // 只要有一个机会能走到,就返回成功
                        if(nx == m - 1 && ny == n - 1) return true;
                        p.add(new int[]{nx, ny});
                        vis[nx][ny] = true;
                    }
                }
            }
            f = spreadFire(grid, onFire, f);
        } 
        return false;
    }

    // 火的扩散 BFS 函数
    private List<int[]> spreadFire(int[][] grid, boolean[][] onFire, List<int[]> f) {
        int m = grid.length, n = grid[0].length;
        List<int[]> tmp = f;
        f = new ArrayList<>();
        for(int[] pos : tmp) {
            int x = pos[0], y = pos[1];
            for(int[] d : DIRS) {
                int nx = x + d[0], ny = y + d[1];
                if(nx >= 0 && nx < m && ny >= 0 && ny < n && !onFire[nx][ny] && grid[nx][ny] == 0) {
                    onFire[nx][ny] = true;
                    f.add(new int[]{nx, ny});
                }
            }
        }
        return f;
    }

    public int maximumMinutes(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        int left = 0, right = m * n;
        // 二分查找的闭区间写法
        while(left <= right) {
            int mid = (left + right) >> 1;
            if(check(grid, mid)) left = mid + 1;
            else right = mid - 1;
        }
        // left > m * n 说明不管停留多久都行
        return left > m * n ? 1000000000 : right;
    }
}