算法基础篇 · 04 前缀和(C++ 题解)

一句话思想:预处理出前缀和数组,把区间求和从 O(n) 降到 O(1)------经典的用空间换时间。
适用场景:多次区间求和查询、最大子段和、二维子矩阵求和等。
前缀和与差分是一对互逆的运算,学完差分后会有更深刻的理解。
本章目录
| 题目 | 来源 | 难度 |
|---|---|---|
| 4.1 一维前缀和 | 牛客网【模板】 | ★ |
| 4.2 最大子段和 | 洛谷 P1115 | ★★ |
| 4.3 二维前缀和 | 牛客网【模板】 | ★ |
| 4.4 激光炸弹 | 洛谷 HNOI2003 | ★★ |
4.1 一维前缀和
- 题目来源:牛客网
- 题目链接:【模板】前缀和
- 难度系数:★
题目描述
给定一个长度为 n 的数组 a₁, a₂, ......, aₙ。
接下来有 q 次查询,每次查询有两个参数 l, r。
对于每个查询,请输出 a_l + a_{l+1} + ... + a_r。
输入描述
- 第一行包含两个整数 n 和 q。
- 第二行包含 n 个整数,表示 a₁, a₂, ......, aₙ。
- 接下来 q 行,每行包含两个整数 l 和 r。
数据范围:1 ≤ n, q ≤ 10⁵;−10⁹ ≤ ai ≤ 10⁹;1 ≤ l ≤ r ≤ n。
输出描述
输出 q 行,每行代表一次查询的结果。
示例
输入:
3 2
1 2 4
1 2
2 3
输出:
3
6
解题思路
前缀和模板题,直接套用「公式」创建前缀和数组,然后利用前缀和数组的「性质」处理 q 次查询。
- 创建前缀和数组 :
f[i] = f[i-1] + a[i] - 查询 l, r 区间和 :
f[r] - f[l-1]
注意:ai 最大 10⁹,n 最大 10⁵,区间和最大可达 10¹⁴,超过 int 范围,必须用
long long。
C++ 参考代码
cpp
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int n, q;
int a[N];
LL f[N]; // 前缀和数组
int main()
{
cin >> n >> q;
for(int i = 1; i <= n; i++) cin >> a[i];
// 处理前缀和数组
for(int i = 1; i <= n; i++)
{
f[i] = f[i - 1] + a[i];
}
// 处理 q 次询问
while(q--)
{
int l, r; cin >> l >> r;
cout << f[r] - f[l - 1] << endl;
}
return 0;
}
复杂度分析
- 时间复杂度:预处理 O(n),每次查询 O(1),总 O(n + q)。
- 空间复杂度:O(n),存储原数组和前缀和数组。
易错点
- 前缀和数组下标从 1 开始,f0 = 0;
- 数据范围大时必须用
long long,否则会溢出; - 查询公式是
f[r] - f[l-1],不是f[r] - f[l]。
4.2 最大子段和
- 题目来源:洛谷
- 题目链接 :P1115 最大子段和
- 难度系数:★★
题目描述
给出一个长度为 n 的序列 a,选出其中连续且非空的一段使得这段和最大。
输入描述
- 第一行是一个整数,表示序列的长度 n。
- 第二行有 n 个整数,第 i 个整数表示序列的第 i 个数字 aᵢ。
数据范围:40% 的数据 n ≤ 10³;100% 的数据 1 ≤ n ≤ 2×10⁵,−10⁴ ≤ aᵢ ≤ 10⁴。
输出描述
输出一行一个整数表示答案。
示例
输入:
7
2 -4 3 -1 2 -4 3
输出:
4
解题思路
关于这道题的解法有很多种,这里介绍利用「前缀和」的解法。
考虑以 i 位置的元素 ai「为结尾」的最大子段和:
- 在求「区间和」时,相当于是用 fi 减去 i 位置前面的某一个 fx;
- 如果想要「最大子段和」,也就是「最大区间和」,那么用 fi 减掉一个前驱最小值即可。
因此,我们可以创建 a 数组的「前缀和」数组,然后在遍历前缀和数组的过程中,一边「更新前驱最小值」,一边「更新当前位置为结尾的最大子段和」。
C++ 参考代码
cpp
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
int n;
LL f[N]; // 前缀和数组
int main()
{
cin >> n;
for(int i = 1; i <= n; i++)
{
LL x; cin >> x;
f[i] = f[i - 1] + x;
}
LL ret = -1e20;
LL prevmin = 0;
for(int i = 1; i <= n; i++)
{
ret = max(ret, f[i] - prevmin);
prevmin = min(prevmin, f[i]);
}
cout << ret << endl;
return 0;
}
复杂度分析
- 时间复杂度:O(n),一遍遍历同时维护前驱最小值和最大子段和。
- 空间复杂度:O(n),存储前缀和数组(也可以优化为 O(1),边读边算)。
易错点
- 答案初始值要设为极小值(如 -1e20),因为所有数可能都是负数;
- 前驱最小值 prevmin 初始为 0(对应 f0);
- 先更新答案再更新前驱最小值,保证前驱最小值只包含 i 之前的位置;
- 子段必须非空,所以不能选空集。
4.3 二维前缀和
- 题目来源:牛客网
- 题目链接:【模板】二维前缀和
- 难度系数:★
题目描述
给你一个 n 行 m 列的矩阵 A,下标从 1 开始。
接下来有 q 次查询,每次查询输入 4 个参数 x₁, y₁, x₂, y₂。
请输出以 (x₁, y₁) 为左上角,(x₂, y₂) 为右下角的子矩阵的和。
输入描述
- 第一行包含三个整数 n, m, q。
- 接下来 n 行,每行 m 个整数,代表矩阵的元素。
- 接下来 q 行,每行 4 个整数 x₁, y₁, x₂, y₂,分别代表这次查询的参数。
数据范围:1 ≤ n, m ≤ 1000;1 ≤ q ≤ 10⁵;−10⁹ ≤ aij ≤ 10⁹;1 ≤ x₁ ≤ x₂ ≤ n;1 ≤ y₁ ≤ y₂ ≤ m。
输出描述
输出 q 行,每行表示查询结果。
示例
输入:
3 4 3
1 2 3 4
3 2 1 0
1 5 7 8
1 1 2 2
1 1 3 3
1 2 3 4
输出:
8
25
32
解题思路
二维前缀和模板题,直接套用「公式」创建前缀和矩阵,然后利用前缀和矩阵的「性质」处理 q 次查询。
-
创建前缀和矩阵:
f[i][j] = f[i-1][j] + f[i][j-1] - f[i-1][j-1] + a[i][j](容斥原理:上面 + 左面 - 重复的左上角 + 当前元素)
-
查询以 (x₁,y₁) 为左上角、(x₂,y₂) 为右下角的子矩阵的和:
f[x₂][y₂] - f[x₁-1][y₂] - f[x₂][y₁-1] + f[x₁-1][y₁-1]
注意:元素最大 10⁹,子矩阵最大 10⁶ 个元素,和最大可达 10¹⁵,必须用
long long。
C++ 参考代码
cpp
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1010;
int n, m, q;
LL f[N][N];
int main()
{
cin >> n >> m >> q;
// 预处理前缀和矩阵
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j++)
{
LL x; cin >> x;
f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + x;
}
}
// 处理 q 次查询
while(q--)
{
int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2;
cout << f[x2][y2] - f[x1 - 1][y2] - f[x2][y1 - 1] + f[x1 - 1][y1 - 1] << endl;
}
return 0;
}
复杂度分析
- 时间复杂度:预处理 O(n×m),每次查询 O(1),总 O(n×m + q)。
- 空间复杂度:O(n×m),存储前缀和矩阵。
易错点
- 前缀和矩阵下标从 1 开始,第 0 行和第 0 列均为 0;
- 创建公式和查询公式都要注意容斥(加重复的部分要减掉);
- 数据范围大时必须用
long long; - 查询时注意是
f[x₁-1]和f[y₁-1],不要写成f[x₁]。
4.4 激光炸弹
- 题目来源:洛谷
- 题目链接 :HNOI2003 激光炸弹
- 难度系数:★★
题目描述
一种新型的激光炸弹,可以摧毁一个边长为 R 的正方形内的所有的目标。
现在地图上有 N(1 ≤ N ≤ 10000)个目标,用整数 Xᵢ, Yᵢ(其值在 0, 5000)表示目标在地图上的位置,每个目标都有一个价值。
激光炸弹的投放是通过卫星定位的,但其有一个缺点,就是其爆破范围,即那个边长为 R 的正方形的边必须和 x, y 轴平行。
若目标位于爆破正方形的边上,该目标将不会被摧毁。
数据范围:1 ≤ n ≤ 10⁴,0 ≤ xᵢ, yᵢ ≤ 5×10³,1 ≤ m ≤ 5×10³,1 ≤ vᵢ ≤ 100。
输入描述
输入文件的第一行为正整数 N 和正整数 R,接下来的 N 行每行有 3 个正整数,分别表示 xᵢ, yᵢ, vᵢ。
输出描述
输出文件仅有一个正整数,表示一颗炸弹最多能炸掉地图上总价值为多少的目标(结果不会超过 32767)。
示例
输入:
2 1
0 0 1
1 1 1
输出:
1
解题思路
可以用一个二维矩阵将所有目标的价值存起来,其中 aij 就表示 i, j 位置的目标价值之和。
一颗炸弹能够获得的价值正好是一个 R×R 大小的一个正方形内所有目标的价值总和,那么我们可以求出 a 矩阵的前缀和矩阵,然后枚举所有边长为 R 的子正方形的价值之和,求出里面的最大值即可。
如何枚举边长为 R 的所有正方形:
- 仅需枚举右下角 x₂, y₂(R+1 ≤ x₂ ≤ 5000, R+1 ≤ y₂ ≤ 5000),那么结合边长 R 就可算出左上角 x₂−R+1, y₂−R+1;
- 代入前缀和矩阵中,就可以快速求出这个正方形内所有目标的总价值。
细节问题:
- 题目中某一个位置会「重复」出现,因此
a[i][j] += w; - 半径 R 可能「超过 5000」,此时炸弹可以摧毁所有目标,也就是整个矩阵的目标价值之和(用
m = min(m, n)处理)。
C++ 参考代码
cpp
#include <iostream>
using namespace std;
const int N = 5010;
int n, m;
int a[N][N];
int f[N][N]; // 前缀和矩阵
int main()
{
cin >> n >> m;
while(n--)
{
int x, y, v; cin >> x >> y >> v;
x++, y++; // 下标从 1 开始计数
a[x][y] += v; // 同一个位置有可能有多个目标
}
n = 5001;
// 预处理前缀和矩阵
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= n; j++)
{
f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + a[i][j];
}
}
int ret = 0;
m = min(m, n); // 如果 m 很大,相当于就是把整个区域全部摧毁
// 枚举所有边长为 m 的正方形
for(int x2 = m; x2 <= n; x2++)
{
for(int y2 = m; y2 <= n; y2++)
{
int x1 = x2 - m + 1, y1 = y2 - m + 1;
ret = max(ret, f[x2][y2] - f[x1 - 1][y2] - f[x2][y1 - 1] + f[x1 - 1][y1 - 1]);
}
}
cout << ret << endl;
return 0;
}
复杂度分析
- 时间复杂度:O(5001²) 预处理 + O(5001²) 枚举,约 5×10⁷ 操作,可通过。
- 空间复杂度:O(5001²) ≈ 100MB(两个 int 矩阵),注意内存限制。
易错点
- 坐标从 0 开始,读入后要
x++, y++转为从 1 开始; - 同一位置可能有多个目标,要用
+=累加价值; - 枚举正方形时右下角从 m 开始(保证左上角 ≥ 1);
- R 可能超过 5000,用
m = min(m, n)处理; - 目标在正方形边上不算被摧毁,所以用闭区间 x₁, x₂ × y₁, y₂ 对应的是内部 R×R 个点(边长 R 的正方形内部有 R×R 个格点)。