ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

一维二维前缀和从原理到模板:C++算法竞赛区间求和实战

2026/10/6 8:29:28 拓冰建站 浏览量
一维二维前缀和从原理到模板:C++算法竞赛区间求和实战 刷题刷到一定阶段你会发现很多问题到最后拼的不是灵光一现而是基础工具的熟练度。前缀和就是这样一个被低估的基础工具尤其二维前缀和它在算法竞赛、信奥和面试题里出现频率极高但很多人对它停留在背公式的层面稍微变形就傻眼。我写这篇博文就是用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]在i0时会出现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。现在我们构造前缀和数组ss[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)肉眼算568928。用公式S[3][3]45S[1][3]6S[3][1]12S[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级别定长数组完全够用别过度工程化。如果用vectorvectorlong long写法上要注意初始化方式别一上来就resize出全零矩阵int n, m; cin n m; vectorvectorlong long s(n 1, vectorlong 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]总是写错我在带人刷题时反复看到一个现象前缀和的代码背得滚瓜烂熟一遇到l0或l1的边界就翻车。比如查询[1, n]整个区间时如果数组是0-based的s[l-1]就访问到s[-1]程序直接崩溃或者返回垃圾值。我的惯用解法就是前面提到的1-based下标让数组从1存到ns[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^10int必然溢出。溢出后的结果可能是负数也可能是个看起来完全随机的数这种错误在本地样例上不太容易暴露一提交就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你会少掉不少头发。希望这篇博文能让你少走我走过的弯路把前缀和真正变成你手里的确定性武器。