1. 激光炸弹问题背景解析
激光炸弹问题源自《算法竞赛进阶指南》中的经典例题,它巧妙地将二维前缀和与差分算法应用于实际场景。题目描述的是:在一个N×N的网格上分布着若干目标点,每个点有其价值。现在需要放置一个R×R的正方形炸弹,求炸弹覆盖区域内目标点总价值的最大值。
这个问题看似简单,但直接暴力求解的时间复杂度高达O(N^4),对于N=5000的典型数据规模完全不可行。而通过二维前缀和优化,我们可以将时间复杂度降至O(N^2),这正是算法竞赛中典型的空间换时间策略。
提示:在实际竞赛中,N=5000时,O(N^2)算法需要处理25,000,000次运算,这在现代CPU上大约需要0.5秒,刚好满足时间限制。
2. 二维前缀和核心原理
2.1 基本概念与构建
二维前缀和的核心思想是预处理一个与原始网格大小相同的数组sum,其中sum[i][j]表示从(1,1)到(i,j)矩形区域内所有元素的和。构建公式为:
cpp复制sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + matrix[i][j]
这个公式通过容斥原理避免了重复计算:将上方和左方的矩形相加后,减去重叠部分(左上角矩形),最后加上当前点的值。
2.2 区域求和推导
给定任意矩形区域(x1,y1)到(x2,y2),其和可以通过前缀和数组快速计算:
cpp复制area_sum = sum[x2][y2] - sum[x1-1][y2] - sum[x2][y1-1] + sum[x1-1][y1-1]
这个推导过程同样基于容斥原理:用大矩形减去左侧和上方的矩形,再加回被重复减去的左上角矩形。
3. 激光炸弹问题解法实现
3.1 算法步骤详解
-
数据预处理:
- 读取所有目标点坐标和价值
- 构建二维数组存储每个格点的价值(注意坐标从1开始)
- 处理坐标重叠情况(同一格点可能有多个目标)
-
构建前缀和数组:
- 按照2.1节的公式逐行计算前缀和
- 边界处理:i=0或j=0时sum[i][j]=0
-
枚举所有可能的炸弹位置:
- 对于每个(i,j)作为炸弹右下角
- 计算炸弹覆盖区域:左上角(max(1,i-R+1), max(1,j-R+1))到右下角(i,j)
- 使用2.2节的公式计算区域和
- 维护全局最大值
3.2 代码实现关键点
cpp复制const int MAX_N = 5005;
int sum[MAX_N][MAX_N];
int main() {
int n, r;
cin >> n >> r;
r = min(r, 5001); // 炸弹尺寸超过地图无意义
// 读取数据并构建原始矩阵
while (n--) {
int x, y, w;
cin >> x >> y >> w;
sum[x+1][y+1] += w; // 转换为1-based坐标
}
// 构建前缀和数组
for (int i = 1; i <= 5001; ++i) {
for (int j = 1; j <= 5001; ++j) {
sum[i][j] += sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1];
}
}
// 枚举所有可能的炸弹位置
int ans = 0;
for (int i = r; i <= 5001; ++i) {
for (int j = r; j <= 5001; ++j) {
int val = sum[i][j] - sum[i-r][j] - sum[i][j-r] + sum[i-r][j-r];
ans = max(ans, val);
}
}
cout << ans << endl;
return 0;
}
4. 算法优化与边界处理
4.1 空间优化技巧
虽然我们声明了5005×5005的数组,但实际上:
- 使用全局数组自动初始化为0
- 可以改用vector<vector
>动态分配(但竞赛中通常不推荐) - 如果内存严格受限,可以滚动数组优化,但会大幅增加代码复杂度
4.2 常见错误与调试
-
坐标偏移错误:
- 题目给定的坐标可能是0-based或1-based
- 解决方案:统一转换为1-based处理,避免边界判断
-
炸弹尺寸超过地图:
- 当R>N时,炸弹实际有效尺寸应为N
- 代码中通过
r = min(r, 5001)处理
-
价值累加错误:
- 同一位置可能有多个目标,必须使用
+=而非=
- 同一位置可能有多个目标,必须使用
-
整数溢出问题:
- 每个点价值≤100,最多5000×5000个点,总价值≤2.5e9
- int类型足够(最大约2e9)
5. 算法扩展与应用
5.1 三维前缀和
激光炸弹问题可以扩展到三维场景,此时前缀和公式变为:
cpp复制sum[i][j][k] = sum[i-1][j][k] + sum[i][j-1][k] + sum[i][j][k-1]
- sum[i-1][j-1][k] - sum[i-1][j][k-1] - sum[i][j-1][k-1]
+ sum[i-1][j-1][k-1]
+ matrix[i][j][k]
对应的立方体区域求和公式也更为复杂,但原理相同。
5.2 动态更新问题
如果题目要求支持动态修改点值,二维前缀和就不再适用。此时可以考虑:
- 二维线段树:单次查询和更新时间为O(logN logN)
- 二维树状数组:实现更简单,但功能受限
6. 竞赛实战技巧
-
输入输出优化:
- 对于N=5000的情况,使用cin/cout可能超时
- 建议添加
ios::sync_with_stdio(false);
-
内存预分配:
- 全局数组比vector更快
- 但要注意题目可能有多组测试数据,需要重置数组
-
调试输出:
- 可以输出小规模案例的前缀和数组验证正确性
- 例如3×3网格的中间过程
-
时间估算:
- 5000×5000的循环大约需要500ms
- 如果TLE,检查是否有不必要的嵌套循环
7. 类似题目推荐
-
一维前缀和:
- 最大子数组和(Kadane算法)
- 区间和查询(LeetCode 303)
-
二维前缀和变种:
- 矩形区域不超过K的最大和(LeetCode 363)
- 统计全1子矩形(LeetCode 1504)
-
差分数组应用:
- 区间加法(LeetCode 370)
- 航班预订统计(LeetCode 1109)
8. 个人实现心得
在实际编码时,我发现以下几个细节特别容易出错:
- 前缀和数组的构建顺序必须先行后列,或者先列后行,不能跳着处理
- 区域求和公式中的四个项非常容易写错符号,建议先画图理解
- 当R=0时需要特判,不过题目通常保证R≥1
一个实用的调试技巧是:先用3×3的小样例手动计算前缀和,再与程序输出对比。例如:
原始矩阵:
1 0 1
0 1 0
1 0 1
正确的前缀和数组应为:
1 1 2
1 2 3
2 3 5
这种小规模验证能快速发现公式实现错误。