力扣1001 网格照明 题解
问题分析
网格照明问题(LeetCode 1001)要求模拟在 n x n 网格上的一系列操作:放置灯和查询位置是否被照亮。核心挑战在于高效处理大规模网格(n 可达 10^9)和大量操作(lamps 和 queries 长度可达 2 * 10^4)。直接存储和遍历网格的二维数组显然不可行。
问题的关键在于理解:一盏灯会照亮其所在的行、列和两条对角线。因此,我们可以通过维护四个哈希表来分别记录被照亮的行、列和两条对角线的状态,从而将空间复杂度从 O(n²) 降低到 O(L + Q),其中 L 是灯的数量,Q 是查询的数量。
算法思路与数据结构设计
1. 核心数据结构
使用四个哈希表(在C++中为 unordered_map)来分别记录:
row:行索引 -> 该行上亮着的灯的数量。col:列索引 -> 该列上亮着的灯的数量。diag1:主对角线(左上-右下)索引 -> 该对角线上亮着的灯的数量。对角线索引可通过行索引 - 列索引计算。diag2:副对角线(右上-左下)索引 -> 该对角线上亮着的灯的数量。对角线索引可通过行索引 + 列索引计算。
此外,需要一个快速查找灯位置的数据结构,用于在关闭周边灯时判断该位置是否真的有灯。使用 unordered_set 存储灯的唯一位置编码(例如 长整型 或 字符串)是高效的选择。
2. 算法步骤
- 初始化 :将所有灯的位置加入
灯位置集合,并更新四个哈希表的计数。 - 处理查询 :
- 对于每个查询位置
(query_x, query_y),检查四个哈希表中对应的计数是否大于0。只要有一个大于0,则该位置被照亮,答案记录为1,否则为0。 - 随后,关闭 查询位置及其周围8个相邻格子(九宫格)内的所有灯。
- 遍历九宫格内的每个位置
(nx, ny)。 - 如果
(nx, ny)在灯位置集合中,则将其从集合中移除,并将四个哈希表中对应的计数减1(如果减到0,可以选择删除该键值对以节省空间)。
- 遍历九宫格内的每个位置
- 对于每个查询位置
- 返回结果:返回所有查询结果的数组。
C++ 代码实现
cpp
#include <vector>
#include <unordered_map>
#include <unordered_set>
using namespace std;
class Solution {
public:
vector<int> gridIllumination(int n, vector<vector<int>>& lamps, vector<vector<int>>& queries) {
// 核心数据结构初始化
unordered_map<int, int> row, col, diag1, diag2;
// 使用长整型编码灯的位置,避免字符串操作开销
unordered_set<long long> lampSet;
// 方向数组,表示查询位置周围的8个邻居和自身(共9个位置)
int dirs[9][2] = {{0,0}, {0,1}, {0,-1}, {1,0}, {-1,0}, {1,1}, {1,-1}, {-1,1}, {-1,-1}};
// 步骤1:放置所有灯,初始化哈希表
for (const auto& lamp : lamps) {
int x = lamp[0];
int y = lamp[1];
long long key = (long long)x * n + y; // 将二维坐标编码为一维唯一键
// 防止重复的灯被重复添加
if (lampSet.insert(key).second) {
row[x]++;
col[y]++;
diag1[x - y]++; // 主对角线索引
diag2[x + y]++; // 副对角线索引
}
}
vector<int> ans;
ans.reserve(queries.size()); // 预分配空间提升效率
// 步骤2:处理每个查询
for (const auto& query : queries) {
int x = query[0];
int y = query[1];
// 判断当前位置是否被照亮:检查对应的行、列、对角线是否有灯
bool isLit = (row[x] > 0) || (col[y] > 0) || (diag1[x - y] > 0) || (diag2[x + y] > 0);
ans.push_back(isLit ? 1 : 0);
// 步骤3:关闭九宫格内的灯
for (const auto& dir : dirs) {
int nx = x + dir[0];
int ny = y + dir[1];
// 检查新坐标是否在网格范围内
if (nx >= 0 && nx < n && ny >= 0 && ny < n) {
long long key = (long long)nx * n + ny;
// 如果该位置有灯,则关闭它
if (lampSet.erase(key)) {
// 从哈希表中移除该灯的影响
if (--row[nx] == 0) row.erase(nx);
if (--col[ny] == 0) col.erase(ny);
if (--diag1[nx - ny] == 0) diag1.erase(nx - ny);
if (--diag2[nx + ny] == 0) diag2.erase(nx + ny);
}
}
}
}
return ans;
}
};
复杂度分析
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(L + Q) | 其中 L 是 lamps 的长度,Q 是 queries 的长度。放置灯为 O(L),每个查询的检查和关灯操作是常数时间(最多检查9个位置)。 |
| 空间复杂度 | O(L) | 主要用于存储 lampSet 和四个哈希表,最坏情况下都与灯的数量 L 成正比。 |
关键点与技巧总结
- 坐标编码 :将二维坐标
(x, y)编码为(long long)x * n + y,确保在unordered_set中唯一且高效。 - 对角线索引计算 :
- 主对角线(斜率1):索引值为
x - y。同一条主对角线上的点,此值相同。 - 副对角线(斜率-1):索引值为
x + y。同一条副对角线上的点,此值相同。
- 主对角线(斜率1):索引值为
- 避免重复灯 :使用
unordered_set的insert方法返回值来判断灯是否已存在,防止哈希表计数错误增加。 - 哈希表清理 :当某个行、列或对角线的灯数量减为0时,使用
erase方法删除该键,有助于减少哈希表的大小,提升后续查询效率(尽管不是必须的)。 - 方向遍历 :使用预定义的方向数组
dirs使代码更简洁,避免手动书写9个条件判断。
此解法通过将空间状态从网格转换到线性的哈希表,巧妙地解决了超大网格带来的内存限制问题,是典型的利用数据结构降维思想的案例。