2257. 统计网格图中没有被保卫的格子数

50 阅读10分钟

在这里插入图片描述

问题

给你两个整数 m 和 n 表示一个下标从 0 开始的 m x n 网格图。同时给你两个二维整数数组 guards 和 walls ,其中 guards[i] = [rowi, coli] 且 walls[j] = [rowj, colj] ,分别表示第 i 个警卫和第 j 座墙所在的位置。

一个警卫能看到 4 个坐标轴方向(即东、南、西、北)的 所有 格子,除非他们被一座墙或者另外一个警卫 挡住 了视线。如果一个格子能被 至少 一个警卫看到,那么我们说这个格子被 保卫 了。

请你返回空格子中,有多少个格子是 没被保卫 的。

示例一

在这里插入图片描述

输入:m = 4, n = 6, guards = [[0,0],[1,1],[2,3]], walls = [[0,1],[2,2],[1,4]] 输出:7 解释:上图中,被保卫和没有被保卫的格子分别用红色和绿色表示。 总共有 7 个没有被保卫的格子,所以我们返回 7

代码示例

import java.util.Arrays;

public class Solution {
    // 定义常量来表示格子的不同状态
    private static final int EMPTY = 0;
    // 表示 警卫
    private static final int GUARD = 1;
    //
    private static final int WALL = 2;
    private static final int PROTECTED = 3; // 可选:直接在 grid 上标记,或用 separate boolean array

    // 四个方向的偏移量: {dx, dy} -> {行变化, 列变化}
    // 分别代表: 上, 下, 左, 右
    private static final int[][] DIRECTIONS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

    public int countUnguarded(int m, int n, int[][] guards, int[][] walls) {
        // 步骤1: 初始化网格
        int[][] grid = new int[m][n]; // 默认都是 EMPTY (0)

        // 步骤2: 标记警卫和墙
        for (int[] guard : guards) {
            grid[guard[0]][guard[1]] = GUARD;
        }
        System.out.println(Arrays.deepToString(grid));
        for (int[] wall : walls) {
            grid[wall[0]][wall[1]] = WALL;
        }
        System.out.println(Arrays.deepToString(grid));
        // 方法一: 使用一个单独的布尔数组记录被保卫的空格子
        // 这样逻辑更清晰,不容易出错
        boolean[][] isProtected = new boolean[m][n];
        
        // 步骤3: 遍历每个警卫,模拟其四个方向的视线
        for (int[] guard : guards) {
            int gx = guard[0];
            int gy = guard[1];
            
            // 尝试四个方向
            for (int[] dir : DIRECTIONS) {
                int dx = dir[0];
                int dy = dir[1];
                
                // 从警卫的下一个格子开始
                int x = gx + dx;
                int y = gy + dy;
                
                // 沿着这个方向一直走,直到越界、遇到墙或遇到另一个警卫
                while (x >= 0 && x < m && y >= 0 && y < n) {
                    // 如果当前格子是墙或警卫,视线被挡住,停止
                    if (grid[x][y] == WALL || grid[x][y] == GUARD) {
                        break;
                    }
                    // 如果当前格子是空地,则它被保卫了
                    if (grid[x][y] == EMPTY) {
                        isProtected[x][y] = true;
                    }
                    // 移动到下一个格子
                    x += dx;
                    y += dy;
                }
            }
        }

        // 方法二: 直接在 grid 上标记 (不推荐初学者用,容易混淆)
        /*
        for (int[] guard : guards) {
            int gx = guard[0];
            int gy = guard[1];
            for (int[] dir : DIRECTIONS) {
                int x = gx + dir[0];
                int y = gy + dir[1];
                while (x >= 0 && x < m && y >= 0 && y < n) {
                    if (grid[x][y] == WALL || grid[x][y] == GUARD) {
                        break;
                    }
                    grid[x][y] = PROTECTED; // 标记为空地已被保卫
                    x += dir[0];
                    y += dir[1];
                }
            }
        }
        */

        // 步骤4: 计数未被保卫的空格子
        int unguardedCount = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 如果是空地 (EMPTY) 并且没有被保卫
                if (grid[i][j] == EMPTY && !isProtected[i][j]) {
                    unguardedCount++;
                }
                // 如果使用方法二,则条件是: if (grid[i][j] == EMPTY) 
            }
        }

        return unguardedCount;
    }

    // ----------------------- 测试代码 -----------------------
    public static void main(String[] args) {
        Solution solution = new Solution();

        int m1 = 4, n1 = 6;
        int[][] guards1 = {{0,0}, {1,1}, {2,3}};
        int[][] walls1 = {{0,1}, {2,2}, {1,4}};
        System.out.println("测试用例1结果: " + solution.countUnguarded(m1, n1, guards1, walls1)); 
        
        // 测试用例2: 更简单的例子
        int m2 = 3, n2 = 3;
        int[][] guards2 = {{1,1}};
        int[][] walls2 = {};
        // 警卫在中心,没有墙。他能看到上下左右四个方向的所有空地。
        // 被保卫的: [0,1], [1,0], [1,2], [2,1] -> 4个
        // 未被保卫的: [0,0], [0,2], [2,0], [2,2] -> 4个 (角落)
        System.out.println("测试用例2结果: " + solution.countUnguarded(m2, n2, guards2, walls2)); // 应该输出 4
        
        // 测试用例3: 警卫被墙挡住
        int m3 = 2, n3 = 2;
        int[][] guards3 = {{0,0}};
        int[][] walls3 = {{0,1}};
        // [0,0]: G, [0,1]: W, [1,0]: 空, [1,1]: 空
        // 警卫[0,0]向右看被墙挡住,向下看能看到[1,0]。
        // [1,1] 无法被看到。
        // 未被保卫的: [1,1] -> 1个
        System.out.println("测试用例3结果: " + solution.countUnguarded(m3, n3, guards3, walls3)); // 应该输出 1
    }
}

整体问题回顾

我们要在一个 m x n 的网格里,计算没有被任何警卫看到的空格子的数量。

  • 警卫 (Guard):占据一个格子,能向上、下、左、右四个方向无限“看”,但视线会被墙 (Wall)另一个警卫挡住。
  • 墙 (Wall):占据一个格子,会阻挡视线。
  • 空格子 (Empty Cell):既不是警卫也不是墙的格子。
  • 被保卫 (Protected):一个空格子如果能被至少一个警卫的视线扫到,就是被保卫的。
  • 目标:找出所有未被保卫的空格子

代码详细解析

1. 定义常量 (Constants)

private static final int EMPTY = 0;
private static final int GUARD = 1;
private static final int WALL = 2;
private static final int PROTECTED = 3; // 可选
  • 作用:用有意义的名字代替数字 0, 1, 2,让代码更清晰、易读、不易出错。
  • EMPTY = 0:代表一个空的、未被占据的格子。Java 中数组默认初始化为 0,所以所有格子一开始都是 EMPTY
  • GUARD = 1:代表这个格子上有一个警卫。
  • WALL = 2:代表这个格子上有一堵墙。
  • PROTECTED = 3:这是一个备选方案。我们可以在 grid 数组里直接用 3 来标记一个空格子已经被保卫了。但作者没有使用这个方案(代码中注释掉了),而是选择用一个单独的布尔数组,这样逻辑更清晰。

2. 定义方向 (Directions)

private static final int[][] DIRECTIONS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
  • 作用:用一个二维数组来表示四个方向的“移动增量”。
  • 解释
    • {-1, 0}:向上移动。行号减 1 (-1),列号不变 (0)。
    • {1, 0}:向下移动。行号加 1 (1),列号不变 (0)。
    • {0, -1}:向左移动。行号不变 (0),列号减 1 (-1)。
    • {0, 1}:向右移动。行号不变 (0),列号加 1 (1)。
  • 为什么这样做? 这样我们就可以用一个 for 循环遍历 DIRECTIONS 数组,轻松地处理四个方向,避免写四遍几乎一样的代码(DRY原则)。

3. 主方法 countUnguarded

这是解决问题的核心函数。

步骤 1: 初始化网格
int[][] grid = new int[m][n]; // 默认都是 EMPTY (0)
  • 创建一个 mn 列的二维整数数组 grid
  • Java 会自动将所有元素初始化为 0,这正好对应我们的 EMPTY 状态。所以,此时整个网格都是空的。
步骤 2: 标记警卫和墙
for (int[] guard : guards) {
    grid[guard[0]][guard[1]] = GUARD;
}
for (int[] wall : walls) {
    grid[wall[0]][wall[1]] = WALL;
}
  • 目的:把题目给的 guardswalls 信息“画”到 grid 网格上。
  • for (int[] guard : guards):这是一个增强型 for 循环 (for-each loop)。它会遍历 guards 数组中的每一个元素(每个元素是一个 [row, col] 的坐标数组)。
  • guard[0] 是行号,guard[1] 是列号。grid[guard[0]][guard[1]] = GUARD; 这行代码就是把警卫所在的位置在 grid 中标记为 1
  • 同理,walls 数组也做同样的操作,标记为 2

此时,grid 网格已经准确地反映了哪些格子是警卫、哪些是墙、哪些是空地。

步骤 3: 模拟警卫的视线 (核心逻辑)
boolean[][] isProtected = new boolean[m][n];
  • 目的:创建一个“被保卫状态”记录表。isProtected[i][j]true 表示位置 [i, j] 的空格子已经被某个警卫看到了。
  • 为什么不用 grid 直接标记? 因为 grid 已经用来记录“物理占据”状态了(谁占了这个位置)。如果我们再用 grid 的值 3 表示“被保卫”,那么一个格子的值就可能同时代表“是空地”和“被保卫”,逻辑容易混乱。用一个独立的布尔数组,职责更单一。
for (int[] guard : guards) {
    int gx = guard[0];
    int gy = guard[1];
    ...
}
  • 再次遍历每一个警卫。对于每个警卫,我们要模拟他四个方向的视线。
for (int[] dir : DIRECTIONS) {
    int dx = dir[0];
    int dy = dir[1];
    ...
}
  • 遍历四个方向。dxdy 就是当前方向的行和列的增量。
int x = gx + dx;
int y = gy + dy;
  • 关键点! 视线从警卫的下一个格子开始!
  • 警卫自己占的格子是 [gx, gy]。他往 (dx, dy) 方向看,第一个看到的格子是 [gx+dx, gy+dy]。我们不能从 [gx, gy] 开始,因为那个位置是警卫自己,不是空格子。
while (x >= 0 && x < m && y >= 0 && y < n) {
    ...
}
  • while 循环:只要坐标 (x, y) 在网格范围内,就继续扫描。
  • 这个循环会沿着一个方向一直走,直到走出网格边界。
if (grid[x][y] == WALL || grid[x][y] == GUARD) {
    break;
}
  • 视线阻挡逻辑:检查当前格子 [x, y]
    • 如果它是墙 (WALL) 或另一个警卫 (GUARD),说明视线被挡住了,后面的格子都看不到了,所以用 break 退出 while 循环,停止这个方向的扫描。
    • 注意:这个格子本身(墙或警卫)不会被标记为“被保卫”,因为题目要求统计的是“空格子”。
if (grid[x][y] == EMPTY) {
    isProtected[x][y] = true;
}
  • 标记被保卫:如果当前格子是空地 (EMPTY),说明警卫的视线能到达这里,这个空格子就被保卫了。
  • 我们在 isProtected 数组中将这个位置标记为 true
x += dx;
y += dy;
  • 移动到下一个格子,继续 while 循环。

总结这个“视线模拟”过程: 想象一个警卫站在 [1,1],他向右看 (dx=0, dy=1)。

  1. 第一个格子:x = 1+0=1, y=1+1=2 -> [1,2]。检查 grid[1][2],如果是 EMPTY,标记 isProtected[1][2]=true。然后 y += 1 -> y=3
  2. 第二个格子:[1,3]。如果是 EMPTY,标记为 truey=4
  3. 第三个格子:[1,4]。如果是 WALL,执行 break,循环结束。[1,4][1,5] 都不会被标记。
步骤 4: 计数未被保卫的空格子
int unguardedCount = 0;
for (int i = 0; i < m; i++) {
    for (int j = 0; j < n; j++) {
        if (grid[i][j] == EMPTY && !isProtected[i][j]) {
            unguardedCount++;
        }
    }
}
return unguardedCount;
  • 目的:遍历整个网格,找出所有“是空地”并且“没有被保卫”的格子。
  • 双重 for 循环:外层 i 遍历每一行,内层 j 遍历每一行中的每一列。
  • 判断条件
    • grid[i][j] == EMPTY:确保这个格子是空的(不是警卫或墙)。
    • !isProtected[i][j]! 是“非”操作符。isProtected[i][j]false 表示这个空格子没有被任何警卫看到。
  • 只有同时满足这两个条件,才将计数器 unguardedCount 加 1。
  • 最后返回这个计数。

4. 测试代码 (main 方法)

这部分代码用来验证我们的算法是否正确。

  • 测试用例1m=4, n=6,警卫在 [0,0][1,1],墙在 [0,1][2,3]
    • 分析:[0,0] 的警卫向下能看到 [1,0], [2,0], [3,0][1,1] 的警卫向左能看到 [1,0],向下能看到 [2,1], [3,1],向右能看到 [1,2], [1,3], [1,4], [1,5]。第0行 [0,2][0,5]、第2行 [2,2], [2,4], [2,5]、第3行 [3,2][3,5] 都看不到。总共 4+1+2+4=11 个。
  • 测试用例23x3 网格,警卫在中心 [1,1],无墙。
    • 分析:警卫能看到上下左右四个边的中心 [0,1], [1,0], [1,2], [2,1]。四个角落 [0,0], [0,2], [2,0], [2,2] 是盲区。答案是 4
  • 测试用例32x2 网格,警卫在 [0,0],墙在 [0,1]
    • 分析:[0,0] 警卫向右看被 [0,1] 的墙挡住。向下看能看到 [1,0][1,1] 无法被看到。答案是 1

运行这些测试,如果输出 11, 4, 1,就说明算法正确。

其核心思想是**“射线投射 (Ray Casting)”**,是解决网格类视线、覆盖问题的经典方法。