☰
一维二维前缀和从原理到模板:C++算法竞赛区间求和实战
2026/10/6 8:29:01 网站建设 项目流程

刷题刷到一定阶段,你会发现很多问题到最后拼的不是"灵光一现",而是基础工具的熟练度。前缀和就是这样一个被低估的基础工具,尤其二维前缀和,它在算法竞赛、信奥和面试题里出现频率极高,但很多人对它停留在"背公式"的层面,稍微变形就傻眼。我写这篇博文,就是用C++把一维前缀和、二维前缀和的原理、推导、模板代码和实战坑一次性讲透,让不同水平的读者都能把它当成顺手工具,而不是死记的模板。

这篇内容不是给纯零基础看的,但也不是只有大神才能看懂。只要你会C++的基本语法(数组、循环、函数),跟着我的思路一步步来,完全能掌握。我会先讲清楚为什么需要前缀和,再从一维推到二维,最后给出可以直接抄的模板代码和常见坑排查,保证你学完能立刻用在实战里。

1. 前缀和到底解决了什么问题:暴力法的天花板在哪

1.1 最直观的场景:区间求和

先从一个最经典的场景说起。假设你有一组数据,存在一个数组a里,比如长度是10万。现在要回答m次询问,每次问你"下标l到r这个闭区间里的所有元素之和是多少"。

新手的第一反应是每次询问都写一个循环,从l加到r。这个思路完全正确,但问题是复杂度。一次询问最多要遍历整个数组,也就是O(n)的复杂度,m次询问就是O(n*m)。当n和m都到10万甚至100万这个数量级时,百万乘百万就是万亿次运算,程序跑完不知道等到什么时候。这就是暴力法的天花板——它不是不能算,是算得太慢。

这时候就要引入预处理的思想。既然数组是静态的,不会反复修改,那我们干脆提前算好一些"汇总信息"存起来,查询的时候直接查表。前缀和就是这样一种预处理策略:用一个新数组s,其中s[i]表示原数组前i个元素的和。有了这个表,想求[l, r]的区间和,直接s[r] - s[l-1]就出来了,单次查询是O(1)的时间。这就是前缀和的核心价值——一次预处理,无数次O(1)查询。

1.2 前缀和的数学本质是什么

前缀和本质上是一种"空间换时间"的思路,这一点很多博主没讲透。我们是在用O(n)的额外空间,换来查询阶段O(n)到O(1)的飞跃。从数学角度看,前缀和数组s满足一个简单的递推关系:s[i] = s[i-1] + a[i]。也就是说,前i项的和等于前i-1项的和加上第i项。

这里有一个容易混淆的点:a的下标从0开始还是从1开始?我最开始学的时候经常被这个问题折磨。C++的数组天然是0-based的,a[0]是第一个元素,但前缀和公式里的s[i-1]在i=0时会出现s[-1],这直接越界。所以算法竞赛里最常见的做法是让数组从下标1开始存,也就是读入时错开一位,下标0的位置空着或者填0。这个看似简单的习惯,可以省掉一整套边界特判,后面二维的时候更能体会到它的好处。

生活化地说,前缀和就像你记账时每天都记一个"累计总额"。比如你从1号开始记开销,今天花了30,累计就是30;明天花了20,累计就是50。你想知道3号到5号花了多少,只要拿5号的累计减去2号的累计就行,不需要把三天的账一笔笔重新加。这就是前缀和的直觉。

1.3 适合学前缀和的人群与应用场景

前缀和不是某个特定比赛的专属技能。信息学奥赛里,它是基础中的基础,几乎所有更复杂的算法(树状数组、线段树)都要先理解前缀和才能往下走。算法面试里,题目经常给一个"静态数组多次查询"的约束,前缀和往往就是最优解。甚至在图像处理和计算机视觉里,有一种叫**积分图(Integral Image)**的技术,本质上就是二维前缀和,用来快速计算图像任意矩形区域的像素和。所以这个东西学好了,收益远超做题本身。

一句话总结:如果你发现自己写的暴力循环里,每次都重复遍历同一个数组,而且数组不会变,你就要警觉——这里可能有前缀和甚至差分的优化空间。

2. 一维前缀和:模板推导与C++实现细节

2.1 从暴力到前缀和的完整推导过程

我们用一个具体的例子走一遍推导过程,这样代码怎么写、为什么这么写就一清二楚了。

假设数组内容如下:

a[1] = 2, a[2] = 3, a[3] = 5, a[4] = 1, a[5] = 4

暴力求[2, 4]的区间和,就是3 + 5 + 1 = 9。现在我们构造前缀和数组s:

s[0] = 0 s[1] = s[0] + a[1] = 2 s[2] = s[1] + a[2] = 5 s[3] = s[2] + a[3] = 10 s[4] = s[3] + a[4] = 11 s[5] = s[4] + a[5] = 15

现在求[2, 4]的和,用s[4] - s[1],也就是11 - 2 = 9。注意,为什么是s[1]而不是s[2]?因为我们要的是从2开始,所以"截止到1的累计"要被减掉。用列式表达就是s[r] - s[l-1]。这里l-1是前缀和公式里最容易被写错的地方,我见过太多人在考场上写s[r] - s[l],结果WA到怀疑人生。自己动手推一次,比背十遍公式都管用。

还有一个初始化的细节:s[0] = 0必须显式设置。这样当l = 1时,s[l-1] = s[0] = 0,减掉0等于不减,逻辑自洽。如果忘了初始化s[0],那就是读到了一个未初始化的垃圾值,结果全错。

2.2 一维前缀和的C++模板代码

直接给出我平时用得最顺手的模板。这里我使用1-based下标,读入时间复杂度O(n),查询时间复杂度O(1)。

#include <bits/stdc++.h> using namespace std; const int MAXN = 100005; long long a[MAXN], s[MAXN]; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; for (int i = 1; i <= n; ++i) { cin >> a[i]; } // 构建前缀和 for (int i = 1; i <= n; ++i) { s[i] = s[i - 1] + a[i]; } // 处理m次区间查询 while (m--) { int l, r; cin >> l >> r; cout << s[r] - s[l - 1] << '\n'; } return 0; }

两个细节值得展开说。第一,数组类型我用long long而不是int。为什么?假设n是10万,每个元素最大是10万,总和就是10的10次方,已经超过int的约21亿上限。一旦溢出,结果就是个负数或者奇怪的值,这种错误特别隐蔽。宁可多占点内存,也别在数据范围上赌运气。第二,ios::sync_with_stdio(false)和cin.tie(nullptr)这两行是C++输入流的加速开关,能大幅提升cin的读取速度,不用改成scanf也能在大多数题目里过。不过如果输入量特别巨大,比如超过100万个数,我仍然会直接用scanf或快读,求稳。

2.3 进阶玩法:前缀和怎么配合其他技巧

一维前缀和除了求区间和,还能求区间内某个值出现的次数、区间内奇数个数、区间内满足某种条件的元素个数等。思路是把"值"和"条件"转换成语义,比如把满足条件的记作1,不满足记作0,再做前缀和,查询时就是O(1)的区间计数。

另外,要区分前缀和和差分。差分是前缀和的逆运算,它解决的是"区间统一加一个值、最后统一查询"的问题。很多题需要两个配合使用:先用差分维护修改,再做前缀和还原出最终数组。如果你只学了前缀和而没学差分,遇到这类题会卡很久。我的建议是,把前缀和、差分、树状数组这三样放一起学,因为它们解决的是"静态区间查询""区间修改单点查询""动态区间查询"三个递进的问题,串起来理解,知识体系才是完整的。

3. 二维前缀和:容斥原理与子矩阵查询模板

3.1 二维前缀和的定义与推导之路

二维前缀和就是一维的升级版,处理的是二维矩阵上的问题:给定一个n x m的矩阵,多次询问某个子矩阵内的所有元素之和。暴力做法同样是每次O(行数×列数)地累加,多次询问后复杂度爆炸。二维前缀和的思路和一维如出一辙——预处理一个同样大小的前缀和矩阵S,让S[i][j]表示从(1,1)到(i,j)这个左上角矩形区域的所有元素和。

关键在于,S[i][j]怎么用已经算好的值推出来?直接S[i-1][j] + S[i][j-1]是不行的,因为S[i-1][j-1]这个区域被加了两次,多算了一遍。所以正确的递推公式是:

S[i][j] = S[i-1][j] + S[i][j-1] - S[i-1][j-1] + a[i][j]

这个公式在数学里叫容斥原理。形象地说:我先加上上方矩形的和,再加左方矩形的和,但左上角那块矩形被重复加了,要减掉一次;最后加上当前格子本身的值。这里三个S的写法是大多数人第一次接触二维前缀和时的"劝退点",但自己拿3×3的小矩阵手算一遍,立刻就懂了。

3.2 子矩阵查询公式:怎么从大矩形里挖出我们要的块

预处理完,查询就爽了。如果想求以(x1, y1)为左上角、(x2, y2)为右下角的子矩阵和,公式是:

sum = S[x2][y2] - S[x1-1][y2] - S[x2][y1-1] + S[x1-1][y1-1]

容斥原理再次登场:总的(1,1)到(x2,y2)矩形包含了太多东西,要减去上方多出的部分S[x1-1][y2]和左方多出的部分S[x2][y1-1],但左上角那块被减了两次,要加回来一次S[x1-1][y1-1]。

我强烈建议你自己写一个小矩阵手工验证一次。比如矩阵:

1 2 3 4 5 6 7 8 9

查询(2,2)到(3,3),肉眼算5+6+8+9=28。用公式:S[3][3]=45,S[1][3]=6,S[3][1]=12,S[1][1]=1,算出来45 - 6 - 12 + 1 = 28。完全吻合。亲手验证过一遍,这个公式就是你的肌肉记忆,而不是死记硬背的符号串。

3.3 二维前缀和的C++完整模板

这里我给出一个带完整读入、预处理和查询的模板。注意下标从1开始,n行m列,矩阵元素用long long存储。

#include <bits/stdc++.h> using namespace std; const int MAXN = 1005; long long a[MAXN][MAXN]; long long S[MAXN][MAXN]; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m, q; cin >> n >> m >> q; // 读入矩阵,从下标1开始 for (int i = 1; i <= n; ++i) { for (int j = 1; j <= m; ++j) { cin >> a[i][j]; } } // 构建二维前缀和 for (int i = 1; i <= n; ++i) { for (int j = 1; j <= m; ++j) { S[i][j] = S[i-1][j] + S[i][j-1] - S[i-1][j-1] + a[i][j]; } } // 查询子矩阵和 while (q--) { int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2; long long ans = S[x2][y2] - S[x1-1][y2] - S[x2][y1-1] + S[x1-1][y1-1]; cout << ans << '\n'; } return 0; }

预处理部分是两层循环嵌套,复杂度O(n*m)。查询是常数时间,所有查询总复杂度O(q)。这个模板在绝大多数题目里可以直接套用,哪怕数据范围到1000×1000也能轻松跑完。要特别留神的是MAXN的设置——开数组时多开几个位置,比如要求n最大1000,你就开1005,防止访问边界时溢出。这种"多开5个"的保守习惯,是我在无数次RE(运行错误)中养成的。

3.4 内存优化与STL版本的写法

二维数组如果开成定长的long long a[MAXN][MAXN],1000×1000大概8MB,还能接受。但如果数据范围变成5000×5000,那内存开销就是200MB,很多平台会MLE。这时候需要用动态二维vector,或者做行前缀和压缩。不过说实话,算法竞赛里二维前缀和的典型数据范围多在1000级别,定长数组完全够用,别过度工程化。

如果用vector<vector<long long>>,写法上要注意初始化方式,别一上来就resize出全零矩阵:

int n, m; cin >> n >> m; vector<vector<long long>> s(n + 1, vector<long long>(m + 1, 0)); for (int i = 1; i <= n; ++i) { for (int j = 1; j <= m; ++j) { cin >> s[i][j]; } } for (int i = 1; i <= n; ++i) { for (int j = 1; j <= m; ++j) { s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1]; } }

注意这里的技巧:直接在原矩阵上累加成前缀和矩阵,省一个a数组。因为s[i][j]在读入后是原始值,累加后覆盖成前缀和。这样内存省了一半,逻辑也没变复杂。很多老手都这样写,你看到不要懵。

4. 经典应用:从区间求和到最大子矩阵

4.1 最大子矩阵问题:把二维巧妙地降到一维

前缀和最有魅力的应用之一,是解决最大子矩阵和问题:在一个矩阵里找一个子矩阵,使得它的元素和最大。暴力枚举左上角和右下角,复杂度O(n^4),完全不可行。但用二维前缀和可以把子矩阵求和降到O(1),那么枚举左上角和右下角还是O(n^4)。不过还有一个更经典的优化思路,利用二维前缀和结合"一维最大子段和"来降到O(n^3)。

思路是这样的:枚举矩阵的上下两条边界up和down,然后在列方向上把每一列的高度区域[up, down]的元素和求出来。这个"每一列的和"可以用行方向的一维前缀和快速求出,得到一个一维数组col_sum[j]。于是问题就变成了求col_sum数组的最大子段和。用Kadane算法一趟扫完,复杂度O(n)。整体枚举上下边界是O(n^2),配合O(n)的扫描,总复杂度O(n^3)。

很多信奥题和面试题就是在这个基础上换壳。比如"最大全1子矩阵"可以先做前缀和统计零的个数,再用二分或双指针配合。我把这个应用放在这里,是想说明一个观点:前缀和不是终点,它是构建更高级算法的乐高积木。吃透二维前缀和,再学这类经典题会快得多。

4.2 图像处理里的积分图:二维前缀和的亲戚

如果你接触过计算机视觉,一定听说过积分图。这个概念其实就是二维前缀和的另一个名字。在一张灰度图像上,构建积分图后,任意矩形区域的像素灰度之和都能在常数时间内算出来。这个特性让积分图成为人脸检测、图像特征提取等任务的加速利器。

也就是说,你在算法题里学的二维前缀和,并不是只能在OJ上产生AC的"纸上功夫"。当数据变成图像像素、查询变成滑动窗口时,原理完全一致。学习的时候如果能跳出"做题"的局限,把前缀和当成一种通用的"快速区域求和"方法论,以后遇到新问题会更敏感:某处如果有频繁的区域聚合查询,就值得考虑前缀和思路。

4.3 配合差分扩展:二维差分与二维前缀和的组合拳

二维前缀和的逆运算,是二维差分。它解决的是"给某个子矩阵所有元素统一加一个值,最后统一输出整个矩阵"的场景。做法是在四个角上做两次加两次减的标记,全部标记完成后,对整个矩阵做一次二维前缀和,就能得到最终矩阵。这个技巧在竞赛题里出现频率同样很高,经常和二维前缀和一起考。

我印象很深的一道题:给定一个初始全0的大矩阵,执行多次"给某个矩形区域加一个数"的操作,最后询问某个点的值。老老实实每次更新区域里的每个格子,复杂度不可接受;但用二维差分,每次操作只改四个位置,最后做一次二维前缀和还原,快得飞起。拿纸笔走一遍「差分标记 → 前缀和还原」的过程,你就能把两个知识点彻底打通。

5. 实战高频坑与排查技巧实录

5.1 边界错误:为什么s[l-1]总是写错

我在带人刷题时反复看到一个现象:前缀和的代码背得滚瓜烂熟,一遇到l=0或l=1的边界就翻车。比如查询[1, n]整个区间时,如果数组是0-based的,s[l-1]就访问到s[-1],程序直接崩溃或者返回垃圾值。

我的惯用解法就是前面提到的1-based下标,让数组从1存到n,s[0]永远是0。这样查询任何区间都天然安全。很多人觉得"C++数组本来就从0开始,人为改成1-based太别扭",但实际用起来你会发现,写循环时for (int i = 1; i <= n; ++i)和for (int i = 0; i < n; ++i)几乎一样顺手,却省掉了一大堆+1、-1的特判。这是一个性价比极高的习惯,强烈推荐养成。

5.2 数据类型溢出:一场发生在int背后的灾难

前缀和最常见的隐藏错误就是整数溢出。如果题目没有明确说结果在int范围内,一律用long long是保平安的做法。C++的int一般是32位,范围大约正负21亿;而long long是64位,可以到9×10^18左右。一个10^5的数组、每个元素10^5,求和就是10^10,int必然溢出。溢出后的结果可能是负数,也可能是个看起来完全随机的数,这种错误在本地样例上不太容易暴露,一提交就WA。

判断数据范围是一个很重要的习惯。养成拿到题先算"最大可能值"的习惯:最大n、最大元素、最多操作次数,相乘或相加后的结果会不会超过两个亿?超过就无脑long long。我还见过有人在查询时用int接收最终答案,但中间过程的s[r] - s[l-1]用long long算,这也是对的,因为long long和int运算时会自动提升为long long,最终结果只要不超int范围就不会出错。

5.3 输入输出性能:别让cin成为你的性能瓶颈

数据规模一大,cin和cout的默认同步机制会成为程序变慢的元凶。默认情况下,C++的cin需要和C的scanf保持同步,导致每次输入都有额外开销。解决办法就是ios::sync_with_stdio(false)关掉同步,以及cin.tie(nullptr)解除cin和cout的绑定。

但这两行也不是万能的。如果输入输出极其庞大,比如一次性读入上百万个数,我建议直接用scanf/printf,或者手写快读快写。在实际竞赛中,我见过无数人因为cin没优化而被卡掉五六十分,这不是算法不行,是输入输出的锅。如果实在想用cin又担心性能,那就把这两行加在最前面,大多数场景足够用了。

5.4 调试技巧:打印整个前缀和矩阵

当你发现结果莫名其妙不对,别急着改公式,先把前缀和数组打印出来人工检查。拿一个2×2或3×3的小矩阵做测试数据,手算一遍前缀和矩阵,再和程序输出对比。我调试二维前缀和题时,几乎必定会写一串临时的for循环打印S[i][j],确认每一格的值都对得上。这个过程看起来很笨,但往往能最快定位出问题——到底是预处理推错了,还是查询公式写错了,一眼就能看出来。

还有一个更实用的技巧:把查询尤其炸裂的边界情况单独测一下,比如(1,1)到(1,1)的单点查询、(1,1)到(n,m)的全矩阵查询。全矩阵查询如果结果恰好等于真个矩阵总和,那基本说明代码问题不大;单点查询则能检验x1-1、y1-1这些边界差索引有没有写对。多准备几组这样的"刁钻输入",能省下大量反复提交试错的时间。

5.5 常见问题速查表

为了方便你按图索骥,我把最常见的问题整理成一张表:

症状大概率原因解决方案
查询结果比预期大很多查询公式里S[x1-1][y1-1]没加回来补上容斥原理中的+ S[x1-1][y1-1]
查询(1,1)时崩溃下标0访问到了[-1]改用1-based下标
大数据量时答案突然变负int溢出核心变量换成long long
输入很大时程序运行超时cin未加速加ios::sync_with_stdio(false); cin.tie(nullptr);
预处理结果全为0读入时下标错位,元素没存进预期位置打印原始数组,检查读入循环
二维数组内存爆炸MAXN开太大或维度配错改用vector动态申请或压缩维度

这张表是我踩过的坑的浓缩版本。每次你遇到"前缀和题WA了且样例本地都对"的情况,先对表自查一遍,往往比自己苦想一个小时更高效。记住,算法的世界里面,错误很少是玄学,大概率是某个细节没做到位。

6. 模板的边界:什么时候用前缀和不合适

6.1 动态修改场景:前缀和解决不了的"在线更新"

前缀和有一个严格的前提:构建完成后,原数组在查询期间不能发生修改。如果题目要求"修改第i个元素的值,然后查询区间和",那么每次修改后前缀和数组都要重新计算,复杂度变成O(n),反而比不用前缀和还差。这个问题本质上是"动态区间查询 + 单点修改",正确工具是树状数组或线段树。

因此判断某个题能不能用前缀和,核心标准是看操作是否离线或静态。原数组不变,或者所有更新都发生在查询之前,那前缀和就是最简洁的解法;一旦更新和查询交替出现,就别硬套前缀和了。这个认知能帮你在考场上快速排除错误思路,要知道"选对算法"和"会写算法"一样重要。

6.2 需要区间的最大值最小值:前缀和不是万能钥匙

前缀和只能高效回答"区间和"或"区间计数"类的问题,因为它保存的是累加信息。如果问你"区间[l, r]的最大值",前缀和就完全帮不上忙,因为最大值的合并方式不能简单地用两个前缀和相减来得到。这种问题要用稀疏表(ST表)、线段树或者莫队算法。

这也是我强调"理解原理,不要背模板"的原因。只有理解前缀和存的是"总和",你才能有意识地在问题里识别"求和"语义,而不是见到区间查询就无脑套前缀和。我曾经见过有人用前缀和去求区间最大值,代码写到一半把自己绕进去,最后把题目改成了求和——这就是典型的工具选型错误。

6.3 空间极度紧张的场景:要算一笔时间与空间的账

二维前缀和的空间复杂度是O(n*m),当矩阵很大但查询很少时,比如10000×10000的矩阵但只有个位数的查询,直接用二位前缀和浪费巨大的内存。这时候暴力计算反而更好。做算法题,时间复杂度和空间复杂度是要一起权衡的,不能只顾一头。我自己的习惯是,先估一下数据规模下前缀和会占多少内存,再决定方案;如果内存能扛住,前缀和的代码又短又稳,自然是首选。

7. 我的实操心得:前缀和怎么学才真正通透

我个人带过不少学算法的同学,最常见的学习误区是"记住了公式,但没有亲手推过"。尤其是二维前缀和的容斥推导,如果不去一个3×3的小矩阵里手动演算,过几天一定会忘,或者一变形就出错。所以我的第一个建议是:拿出一张纸,从一维推到二维,把所有公式亲自动手算一遍。这个过程花不了10分钟,但收益远超刷十道同质化的题。

第二个建议是:把前缀和、差分、树状数组放在一起学。它们就是一个递进序列——静态区间求和用前缀和;区间修改、最后统一查询用差分;动态单点修改加区间求和用树状数组;再往后动态区间修改加区间查询,用线段树或树状数组的进阶技巧。一条线串下来,你会形成一种"算法兵器谱"的感觉。看到题目条件,条件反射地匹配到正确的工具,这才是真正的竞争力。

最后再分享一个小技巧:做题时养成"先算极端数据范围"的习惯。前缀和的题几乎必考溢出,你每次写long long都花不了半秒,但能规避一大类WA。每当我看到有人在代码里写int s[MAXN];时,都会劝一句:把这行改成long long,你会少掉不少头发。希望这篇博文能让你少走我走过的弯路,把前缀和真正变成你手里的确定性武器。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询