问题
给你两个整数 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)
- 创建一个
m行n列的二维整数数组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;
}
- 目的:把题目给的
guards和walls信息“画”到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];
...
}
- 遍历四个方向。
dx和dy就是当前方向的行和列的增量。
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)。
- 第一个格子:
x = 1+0=1, y=1+1=2->[1,2]。检查grid[1][2],如果是EMPTY,标记isProtected[1][2]=true。然后y += 1->y=3。 - 第二个格子:
[1,3]。如果是EMPTY,标记为true。y=4。 - 第三个格子:
[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 方法)
这部分代码用来验证我们的算法是否正确。
- 测试用例1:
m=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个。
- 分析:
- 测试用例2:
3x3网格,警卫在中心[1,1],无墙。- 分析:警卫能看到上下左右四个边的中心
[0,1],[1,0],[1,2],[2,1]。四个角落[0,0],[0,2],[2,0],[2,2]是盲区。答案是4。
- 分析:警卫能看到上下左右四个边的中心
- 测试用例3:
2x2网格,警卫在[0,0],墙在[0,1]。- 分析:
[0,0]警卫向右看被[0,1]的墙挡住。向下看能看到[1,0]。[1,1]无法被看到。答案是1。
- 分析:
运行这些测试,如果输出 11, 4, 1,就说明算法正确。
其核心思想是**“射线投射 (Ray Casting)”**,是解决网格类视线、覆盖问题的经典方法。