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

算法基础篇 · 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 次查询。

  1. 创建前缀和数组f[i] = f[i-1] + a[i]
  2. 查询 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. 前缀和数组下标从 1 开始,f0 = 0;
  2. 数据范围大时必须用 long long,否则会溢出;
  3. 查询公式是 f[r] - f[l-1],不是 f[r] - f[l]

4.2 最大子段和

题目描述

给出一个长度为 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),边读边算)。

易错点

  1. 答案初始值要设为极小值(如 -1e20),因为所有数可能都是负数;
  2. 前驱最小值 prevmin 初始为 0(对应 f0);
  3. 先更新答案再更新前驱最小值,保证前驱最小值只包含 i 之前的位置;
  4. 子段必须非空,所以不能选空集。

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 次查询。

  1. 创建前缀和矩阵

    复制代码
    f[i][j] = f[i-1][j] + f[i][j-1] - f[i-1][j-1] + a[i][j]

    (容斥原理:上面 + 左面 - 重复的左上角 + 当前元素)

  2. 查询以 (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. 前缀和矩阵下标从 1 开始,第 0 行和第 0 列均为 0;
  2. 创建公式和查询公式都要注意容斥(加重复的部分要减掉);
  3. 数据范围大时必须用 long long
  4. 查询时注意是 f[x₁-1]f[y₁-1],不要写成 f[x₁]

4.4 激光炸弹

题目描述

一种新型的激光炸弹,可以摧毁一个边长为 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
  • 代入前缀和矩阵中,就可以快速求出这个正方形内所有目标的总价值。

细节问题

  1. 题目中某一个位置会「重复」出现,因此 a[i][j] += w
  2. 半径 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 矩阵),注意内存限制。

易错点

  1. 坐标从 0 开始,读入后要 x++, y++ 转为从 1 开始;
  2. 同一位置可能有多个目标,要用 += 累加价值;
  3. 枚举正方形时右下角从 m 开始(保证左上角 ≥ 1);
  4. R 可能超过 5000,用 m = min(m, n) 处理;
  5. 目标在正方形边上不算被摧毁,所以用闭区间 x₁, x₂ × y₁, y₂ 对应的是内部 R×R 个点(边长 R 的正方形内部有 R×R 个格点)。

下一篇:算法基础篇 · 05 差分(C++ 题解)

相关推荐
汉克老师2 小时前
CSP-J 2026 初赛试题解析(第三部分:完善程序题(第一题))精讲
c++·csp-j·小学生·学c++编程
青山是哪个青山2 小时前
LeetCode 123:买卖股票的最佳时机 III
算法
haluhalu.2 小时前
初识 Protobuf:微服务跨语言通信的一份契约
java·c语言·开发语言·c++·python
Zane19942 小时前
写对二分查找有多难?Java集合框架的作者也曾栽在一行mid计算上
算法
Lokey8682 小时前
transformer结构
算法
罗斯8393 小时前
EMBER恶意软件基准数据集
人工智能·算法·安全·网络安全
longxuan013 小时前
倍增算法详解
c++·st表·倍增算法
-森屿安年-3 小时前
返回最大数
c++·贪心算法
0+1113 小时前
算法 --二分查找
c++·算法·leetcode