统计网格图中没有被保卫的格子数 图解题解
这道题到底在问什么
- 输入
- m=4, n=5, guards=[[0,0],[2,3]], walls=[[0,2],[2,1],[3,3]]
- 输出
- 7
- 输入
- m=3, n=3, guards=[[1,1]], walls=[[0,1],[1,0],[2,1],[1,2]]
- 输出
- 4
最优解:为什么这么做
一句话答案:LeetCode 2257 统计网格中没有被保卫的格子数:射线标记法,把警卫和墙都当挡板,每个警卫朝四个方向逐格标『被看守』、遇挡板或边界停,最后数仍是空的格子,时间 O(m·n)。
既不是警卫墙、又没被看到的空格有几个
给一张 m 行 n 列的网格,上面放了若干警卫和墙。每个警卫朝上下左右四个方向看,视线一直延伸,直到撞上一堵墙或另一个警卫才停下,沿途空格就算被保卫。问既不是警卫、也不是墙、又没被任何警卫看到的空格有几个。题面例子 m=4、n=5,警卫在 (0,0) 和 (2,3),墙在 (0,2)、(2,1)、(3,3),答案是 7。
站在每个空格反着找警卫,慢在哪
一种想法是站在每个空格上,朝四个方向各望出去,看沿途有没有警卫、路上有没有墙挡着。这能得出答案,可 m·n 个空格每个都要往外扫四条线,同一段空地被反复走,一多就拖。既然视线本来就是从警卫发出的,不如让警卫主动往外射:一条视线扫过的空格顺手全标上,每段空地只走一遍。
让警卫主动射线,遇挡板就收手
把网格开成一张表,先把每个警卫、每堵墙都记成挡板值 2。再对每个警卫沿四个方向发射线。射线,就是从警卫脚下沿一个方向一路走到底:每往前一步,只要下一格还在网格里、又不是挡板,就把它标成 1、记作被看守,继续推进;一旦下一格是墙、是另一个警卫或已越出边界,这条线立刻收住。所有警卫都射完,表里还是 0 的格子就是没被保卫的空格,数出来即答案。
四个方向怎么编码,每步为何先判边界
四个方向用一个小技巧编码:方向数组写成 (-1,0,1,0,-1),相邻两两配对——(-1,0) 是上、(0,1) 是右、(1,0) 是下、(0,-1) 是左,正好凑齐四个偏移,省去四段几乎一样的代码。推进时守住一条:每落脚之前先看下一格在不在界内、是不是挡板,两个条件都过才前进、才标 1。先判界再走,警卫贴边时向外一步就出界、循环自然拦下,不会越出数组。
拿题面这张 4 行 5 列网格亲手射一遍
先把 (0,0)、(2,3) 两个警卫和 (0,2)、(2,1)、(3,3) 三堵墙都置成 2。轮到警卫 (0,0):向上一步就出界、跳过;向右到 (0,1) 标 1,再走一步撞上墙 (0,2) 停;向下依次标 (1,0)、(2,0)、(3,0) 到底;向左出界跳过——它照亮了 4 格。再轮到警卫 (2,3):向上标 (1,3)、(0,3) 到顶;向右标 (2,4) 到边;向下第一步就是墙 (3,3)、直接停;向左标 (2,2)、再走一步撞上墙 (2,1) 停——又照亮 4 格。八格标完,整张表里还是 0 的格子共 7 个,就是没被保卫的空格。
撞上挡板还往后标,答案就偏小
复杂度上,开表、射线标记、最后数 0 各遍历一遍;警卫和墙把每行每列切成若干段空地,每段最多被两侧警卫各扫一次,单格在横竖两个方向都只被常数次经过,时间 O(m·n)。那张 m·n 的状态表是主要开销,没有额外递归或队列,空间 O(m·n)。
最容易出岔的是收线时机:视线撞到值为 2 的格子必须当场停,多标一格就把墙后本看不到的空格算进去,答案偏小;只把警卫记成挡板、忘了墙也挡视线,视线便会穿墙而过,凭空多标一片。边界也要想清:警卫被墙贴身围住时四个方向一步都迈不出,答案就是全部空格;整张网格一个空格都没有、或唯一那个空格恰好被照到,答案都是 0。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记牢这一句:警卫和墙都是 2 号挡板,视线沿四个方向推进,遇到挡板或边界就停,沿途空格标 1。最后还是 0 的空格就是答案。下面把网格铺开,一个警卫一个警卫地射。
- 4橙=警卫 红=墙 灰=空格这是初始的 4 行 5 列网格。橙色的两格是警卫,分别在 (0,0) 和 (2,3);红色的三格是墙,在 (0,2)、(2,1)、(3,3)。剩下的灰色格子就是 15 个空格,现在一个都还没被照到。警卫和墙在算法里都记成 2,是视线撞上就得停的硬挡板。
- 5当前警卫 (0,0)从第一个警卫开始。把镜头对准 (0,0) 这个警卫,它要朝上、右、下、左四个方向依次射出视线。约定按上右下左的顺序走,每个方向都从警卫脚下往外一步一步推进,把碰到的空格点亮成绿色,直到撞墙、撞另一个警卫或者射出边界才收手。
- 6向上越界警卫 (0,0) 紧贴着网格的边,向上第一步就出界了,这个方向一格都照不到,直接跳过。
- 7警卫 (0,0) 向右警卫 (0,0) 的视线向右推进一步,来到 (0,1)。这一格原来是空的,现在被视线扫到,标成 1、点亮成绿色,表示它被保卫了。已保卫的空格数变成 1。视线还没撞到挡板,接着往右看下一格。
- 8向右撞挡板视线继续向右走,下一格 (0,2) 是一堵墙,值是 2,是硬挡板。视线被它挡住,(0,2) 后面的格子这条射线就照不到了,停线。
- 9警卫 (0,0) 向下警卫 (0,0) 的视线向下推进一步,来到 (1,0)。这一格原来是空的,现在被视线扫到,标成 1、点亮成绿色,表示它被保卫了。已保卫的空格数变成 2。视线还没撞到挡板,接着往下看下一格。
- 10警卫 (0,0) 向下警卫 (0,0) 的视线向下推进一步,来到 (2,0)。这一格原来是空的,现在被视线扫到,标成 1、点亮成绿色,表示它被保卫了。已保卫的空格数变成 3。视线还没撞到挡板,接着往下看下一格。
- 11警卫 (0,0) 向下警卫 (0,0) 的视线向下推进一步,来到 (3,0)。这一格原来是空的,现在被视线扫到,标成 1、点亮成绿色,表示它被保卫了。已保卫的空格数变成 4。视线还没撞到挡板,接着往下看下一格。
- 12向下越界向下这条射线一直照到网格边上,再往下就出界了,没有格子可照,这条射线到此为止。刚才点亮的几格记录在右边面板里。
- 13向左越界警卫 (0,0) 紧贴着网格的边,向左第一步就出界了,这个方向一格都照不到,直接跳过。
- 14警卫 (0,0) 完成警卫 (0,0) 的四个方向都射完了。它照亮了自己右边一格和下边一列,目前已保卫的空格数是 4。接着处理下一个警卫。
- 15当前警卫 (2,3)轮到第二个警卫。把镜头对准 (2,3) 这个警卫,它要朝上、右、下、左四个方向依次射出视线。约定按上右下左的顺序走,每个方向都从警卫脚下往外一步一步推进,把碰到的空格点亮成绿色,直到撞墙、撞另一个警卫或者射出边界才收手。
- 16警卫 (2,3) 向上警卫 (2,3) 的视线向上推进一步,来到 (1,3)。这一格原来是空的,现在被视线扫到,标成 1、点亮成绿色,表示它被保卫了。已保卫的空格数变成 5。视线还没撞到挡板,接着往上看下一格。
- 17警卫 (2,3) 向上警卫 (2,3) 的视线向上推进一步,来到 (0,3)。这一格原来是空的,现在被视线扫到,标成 1、点亮成绿色,表示它被保卫了。已保卫的空格数变成 6。视线还没撞到挡板,接着往上看下一格。
- 18向上越界向上这条射线一直照到网格边上,再往上就出界了,没有格子可照,这条射线到此为止。刚才点亮的几格记录在右边面板里。
- 19警卫 (2,3) 向右警卫 (2,3) 的视线向右推进一步,来到 (2,4)。这一格原来是空的,现在被视线扫到,标成 1、点亮成绿色,表示它被保卫了。已保卫的空格数变成 7。视线还没撞到挡板,接着往右看下一格。
- 20向右越界向右这条射线一直照到网格边上,再往右就出界了,没有格子可照,这条射线到此为止。刚才点亮的几格记录在右边面板里。
- 21向下撞挡板警卫 (2,3) 向下第一步就是 (3,3),那里正好是一堵墙,值为 2。视线一出门就被挡死,这个方向一格都照不到。
- 22警卫 (2,3) 向左警卫 (2,3) 的视线向左推进一步,来到 (2,2)。这一格原来是空的,现在被视线扫到,标成 1、点亮成绿色,表示它被保卫了。已保卫的空格数变成 8。视线还没撞到挡板,接着往左看下一格。
- 23向左撞挡板视线继续向左走,下一格 (2,1) 是一堵墙,值是 2,是硬挡板。视线被它挡住,(2,1) 后面的格子这条射线就照不到了,停线。
- 24警卫 (2,3) 完成警卫 (2,3) 的四个方向都射完了。两个警卫都处理完了,网格里所有被保卫的空格都已经变绿,一共 8 个。下面把剩下的灰格数出来。
- 25答案 = 7两个警卫都射完了,现在数账。一共 15 个空格,被视线照到、变绿的有 8 个,剩下没被任何视线扫到的空格有 7 个,我把它们标成蓝色。像 (0,4)、(1,1) 这些格子,四周的视线要么被墙拦下、要么根本射不到,所以谁也看不见它们。15 减 8 等于 7,这就是没被保卫的格子数。
- 26答案 = 7回放一遍。橙色是警卫,红色是墙,绿色是被视线照到的空格,蓝色这 7 格是没被保卫的。仔细看会发现,警卫 (0,0) 向右的视线被墙 (0,2) 挡住,所以右上角一带照不全;警卫 (2,3) 向下一步就撞上墙 (3,3),底下那格也没人管。挡板决定了视线的边界,数出蓝色格子共 7 个,答案就是它。
⚠️ 容易写错的地方
✗ 错:视线撞到墙或警卫后还继续往后标记
✓ 对:遇到值为 2 的格子立刻停这条射线
墙和警卫会挡住视线,后面的格子看不到。继续标记会把不该保卫的格子算进去,答案偏小
✗ 错:把警卫和墙也数进没被保卫的格子里
✓ 对:只数值仍为 0 的空格
题目问的是空格中没被保卫的数目,警卫格和墙格本身不参与计数,它们的值是 2 不是 0,天然被排除
✗ 错:只把警卫标成挡板,忘了墙也挡视线
✓ 对:警卫和墙都要先置成 2
题面说视线被墙或另一个警卫挡住,两者都是硬挡板,漏掉墙会让视线穿墙而过,多标很多格子
✗ 错:警卫贴边时向外一步就出界还硬走
✓ 对:每步先判下一格是否在界内
不判边界会数组越界,或把视线错误地绕到对面行列,先判界内再前进才安全
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def countUnguarded(
self, m: int, n: int, guards: List[List[int]], walls: List[List[int]]
) -> int:
g = [[0] * n for _ in range(m)]
for i, j in guards:
g[i][j] = 2
for i, j in walls:
g[i][j] = 2
dirs = (-1, 0, 1, 0, -1)
for i, j in guards:
for a, b in pairwise(dirs):
x, y = i, j
while 0 <= x + a < m and 0 <= y + b < n and g[x + a][y + b] < 2:
x, y = x + a, y + b
g[x][y] = 1
return sum(v == 0 for row in g for v in row)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
int countUnguarded(int m, int n, vector<vector<int>>& guards, vector<vector<int>>& walls) {
int g[m][n];
memset(g, 0, sizeof(g));
for (auto& e : guards) {
g[e[0]][e[1]] = 2;
}
for (auto& e : walls) {
g[e[0]][e[1]] = 2;
}
int dirs[5] = {-1, 0, 1, 0, -1};
for (auto& e : guards) {
for (int k = 0; k < 4; ++k) {
int x = e[0], y = e[1];
int a = dirs[k], b = dirs[k + 1];
while (x + a >= 0 && x + a < m && y + b >= 0 && y + b < n && g[x + a][y + b] < 2) {
x += a;
y += b;
g[x][y] = 1;
}
}
}
int ans = 0;
for (auto& row : g) {
ans += count(row, row + n, 0);
}
return ans;
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public int countUnguarded(int m, int n, int[][] guards, int[][] walls) {
int[][] g = new int[m][n];
for (int[] e : guards) {
g[e[0]][e[1]] = 2;
}
for (int[] e : walls) {
g[e[0]][e[1]] = 2;
}
int[] dirs = {-1, 0, 1, 0, -1};
for (int[] e : guards) {
for (int k = 0; k < 4; ++k) {
int x = e[0], y = e[1];
int a = dirs[k], b = dirs[k + 1];
while (x + a >= 0 && x + a < m && y + b >= 0 && y + b < n && g[x + a][y + b] < 2) {
x += a;
y += b;
g[x][y] = 1;
}
}
}
int ans = 0;
for (int[] row : g) {
for (int v : row) {
if (v == 0) {
++ans;
}
}
}
return ans;
}
}复杂度
时间
O(m·n)
开表和最后数 0 都是遍历一遍所有 m 乘 n 个格子。警卫和墙把每一行每一列切成若干空段,每个空段最多被两侧的警卫各扫一次,所以单格在水平方向和竖直方向都只被常数次经过,所有射线加起来经过的格子总数不超过格子数的常数倍,整体随格子数线性增长
空间
O(m·n)
按峰值算。主要开销是那张 m 乘 n 的网格表 g,用来记录每格的状态。没有额外的递归或队列,空间就是这张表本身的大小
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 统计网格图中没有被保卫的格子数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么不站在每个空格上反过来找警卫?+
反过来对每个空格朝四个方向找最近的警卫也能得到答案,但 m·n 个空格每个都要往外扫四条线,同一段空地被反复走,重复劳动多。从警卫出发射线,警卫和墙把每行每列切成一段段空地,每格在横竖两个方向都只被常数次经过,总的经过格数是格子数量级,更省。两种视角答案一致,正向射线写起来也更直接。
为什么警卫和墙都要先置成 2,只标警卫行不行?+
题面说视线被墙或另一个警卫挡住,两者都是硬挡板。如果只把警卫记成挡板、忘了墙,射线遇到墙不会停,会穿墙继续往后标,把墙后本来看不到的空格也算成被保卫,答案就偏小。把警卫和墙统一置成 2,射线每步判断『下一格是不是 2』时两种挡板一视同仁,一个条件就挡住了。
复杂度为什么是 O(m·n),射线不会重复扫很多遍吗?+
不会。警卫和墙把每一行、每一列切成若干段连续空地,每一段最多被它两端的警卫各扫一次,所以任何一格在水平方向和竖直方向都只会被常数次经过。所有射线加起来经过的格子总数不超过格子数的常数倍,再加上开表和最后数 0 各遍历一遍,合起来就是 O(m·n)。空间就是那张 m·n 的状态表,O(m·n)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 统计网格图中没有被保卫的格子数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。