资讯动态

前缀和与差分:从O(n)到O(1)的数组区间操作优化

发布时间:2026/8/15 4:19:23 来源:尧图企业网站定制
1. 从“算得快”到“算得巧”为什么我们需要前缀和与差分如果你写过代码处理过数组那你一定遇到过这样的场景给你一个数组然后反复问你“从第i个元素到第j个元素的和是多少”。新手的第一反应往往是写个循环老老实实地把区间里的数一个个加起来。这没错逻辑清晰结果正确。但当你面对的是一个长度上万、甚至上百万的数组而这样的查询请求每秒要来成千上万次时那个简单的循环就会瞬间成为性能的瓶颈让你的程序慢如蜗牛。这就是前缀和Prefix Sum与差分Difference这对“算法兄弟”登场的时刻。它们不是什么高深莫测的数学魔法而是两种极其朴素却威力巨大的预处理思想。核心目标就一个用一次性的、稍多一点的计算成本把后续无数次重复查询或修改的代价降到最低实现从O(n)到O(1)的质变。简单说就是用“空间换时间”并且换得极其划算。我最初接触这个概念是在处理用户行为日志的实时统计时。我们需要在仪表盘上实时展示“过去1小时内的点击量”、“今日累计活跃用户”等指标。数据流源源不断查询请求此起彼伏。如果每次查询都去扫描原始日志数据库早就崩了。正是前缀和的思想让我们提前算好每个时间点的累积值查询时只需做一次减法系统压力骤减。而差分则在处理“给某个区间所有元素同时加一个值”这类批量更新操作时展现了类似的优雅。网络上大家搜索的“二维前缀和”、“树上差分”甚至是硬件领域的“差分放大电路”、“差分信号”虽然领域迥异但其底层“通过预处理或构造辅助量将复杂操作转化为简单操作”的核心思想是相通的。今天我们就抛开那些复杂的扩展聚焦最基础、最核心的一维前缀和与差分把它们的原理、关联和实战用法彻底讲透。你会发现理解它们真的不需要五分钟。2. 前缀和如何把区间求和变成“小学生减法”让我们先彻底搞定前缀和。它的概念直白得惊人对于一个给定的数组a假设下标从1开始长度为n我们构造一个新的数组s其中s[i]表示原数组a从第一个元素到第i个元素的总和。用公式表示就是s[i] a[1] a[2] ... a[i]这个新数组s就是前缀和数组。2.1 前缀和数组的构建一个递推过程你不需要每次都从头加起。观察一下s[i]其实就是s[i-1]再加上当前的a[i]。所以我们可以用一次遍历轻松构建# 假设原始数组 a长度为 n (下标从1开始a[0]闲置或作它用) s [0] * (n 1) # 多开一位让s[0]0便于统一处理 for i in range(1, n 1): s[i] s[i-1] a[i]看s[0]被初始化为0。这非常关键它使得s[1] s[0] a[1] a[1]成立并且让后续的区间求和公式变得无比简洁。为什么下标从1开始这是算法竞赛和许多工程实践中的常见技巧可以避免很多边界条件的判断。例如当你想求a[1]到a[1]的和时公式s[1] - s[0]依然有效因为s[0]0。如果从0开始求a[0]到a[0]的和就会涉及s[-1]需要额外处理。当然从0开始完全可以只是公式需要稍作调整个人更推荐从1开始的写法心智负担更小。2.2 区间求和的魔法从O(n)到O(1)现在灵魂问题来了有了前缀和数组s如何求原数组a中从第l个元素到第r个元素的和记为sum(l, r)我们不要直接去想a[l] ... a[r]。换个角度s[r]代表了a[1] a[2] ... a[r]s[l-1]代表了a[1] a[2] ... a[l-1]那么从s[r]这个“总包”里扣掉前面不需要的部分s[l-1]剩下的不就是我们想要的a[l] ... a[r]了吗所以公式诞生了sum(l, r) s[r] - s[l-1]举个例子数组a [0, 1, 3, -2, 5](a[0]忽略)。前缀和s [0, 1, 4, 2, 7]。 求a[2]到a[4]的和即3 (-2) 5 6。 用公式sum(2, 4) s[4] - s[1] 7 - 1 6。完全正确。这个操作的复杂度是 O(1)无论区间多长。原本需要循环r-l1次的加法现在只需要两次数组访问和一次减法。当查询次数q很大时总复杂度从可怕的O(q * n)降到了O(n q)O(n)用于构建前缀和O(q)用于响应查询。这就是质变。2.3 实战心得与边界陷阱心得一警惕整数溢出这是最容易踩的坑。前缀和s[i]是累积和其数值范围可能远超原始数组a中单个元素的范围。例如a是int32类型的数组元素值在百万级别数组长度上万那么前缀和很容易超过int32的范围约21亿。务必根据数据范围选择合适的数据类型比如long long(C)、int64(Go/Python自动处理大整数)。心得二s[0] 0是灵魂我见过不少初学者忘记初始化s[0]或者错误地将其设为a[0]。记住s[0]表示“前0个元素的和”逻辑上就是0。这个定义让我们的公式s[r] - s[l-1]在l1时依然成立s[0]被减去。如果你坚持从下标0开始存储原数组那么公式会变为sum(l, r) s[r] - (l 0 ? s[l-1] : 0)代码会多一个条件判断不够优雅。心得三不止于求和前缀和的思想可以推广。求区间和是最典型的应用但“和”可以替换为其他满足结合律的运算比如区间乘积、区间按位与/或等。只要你能定义出一个“前缀累积量”并且这个运算存在逆运算减法是加法的逆运算就能实现类似的O(1)查询。当然像乘法逆运算是除法但需要处理除零和精度问题位运算的逆运算不一定总是存在需要具体分析。3. 差分如何让区间批量更新“静悄悄”如果说前缀和是“查询加速器”那么差分就是“更新优化器”。它解决的是另一类高频问题频繁地对原始数组的某个区间进行批量修改例如给a[l]到a[r]的每个数都加上一个值c。最笨的办法依然是遍历区间逐个元素加上c。一次更新是 O(n)m次更新就是 O(m*n)不堪重负。差分提供了另一种预处理思路。3.1 差分数组的定义与构建对于一个原始数组a我们定义它的差分数组d满足a[i] d[1] d[2] ... d[i]或者等价地d[i] a[i] - a[i-1](对于 i 1)且d[1] a[1](假设a[0] 0)。简单说差分数组d的第i项就是原始数组a中相邻两项的差。你会发现一个美妙的关系原始数组a是差分数组d的前缀和数组。构建差分数组非常简单# a 是原始数组长度 n (下标从1开始) d [0] * (n 2) # 有时会多开一点防止后续操作越界 d[1] a[1] for i in range(2, n 1): d[i] a[i] - a[i-1]3.2 差分的神奇操作O(1)完成区间更新现在考虑对原始数组a的区间[l, r]统一加上一个值c。如果直接修改a需要修改r-l1个元素。但如果我们操作的是差分数组d魔法就发生了。我们只需要做两步d[l] cd[r1] - c(如果 r1 没有越界)为什么这样是对的让我们从定义出发。a[i]是d[1]到d[i]的和。对于i la[i]的求和范围不包含d[l]和d[r1]所以不受影响。对于l i ra[i]的求和范围包含了d[l]它被加了c但不包含d[r1]因为i r r1。所以a[i]的值相当于比原来多了c。对于i ra[i]的求和范围同时包含了d[l]c和d[r1]-c一加一减抵消了所以a[i]也不受影响。看通过修改差分数组的两个点我们间接地完成了对整个区间的批量更新时间复杂度是 O(1)举个例子a [0, 1, 3, -2, 5]。先构建差分d [0, 1, 2, -5, 7](因为 3-12, -2-3-5, 5-(-2)7)。 现在想给a[2]到a[4]的每个数加 10。 操作差分数组d[2] 10d[2]变为 12d[5] - 10d[5]变为 -3 (注意r15我们数组长度是5所以有d[5])。 此时d [0, 1, 12, -5, -3]。 那么新的a是什么呢用a[i] d[1]...d[i]还原a[1] 1a[2] 11213(原310正确)a[3] 112(-5)8(原-210正确)a[4] 112(-5)(-3)5(原510等等这里错了)问题出在哪我们原始的a[4]是5加10应该是15。但计算结果是5。仔细检查我们的原始数组长度是5下标1-4a[4]是最后一个元素。当我们给d[5]减去10时a[4]的计算公式是d[1]d[2]d[3]d[4]根本不包含d[5]所以d[5]的修改对a[4]无效。这就是一个关键边界陷阱当r是数组最后一个下标时r1会越界。我们的操作d[r1] - c可能没有意义因为那个位置不属于我们关注的a数组的有效范围。实际上对于最后一个元素的区间更新我们只需要做d[l] c即可因为后续没有元素需要被“抵消”了。在代码中我们通常通过多开一位数组空间来统一处理把d的长度设为n2这样d[r1]总是有效的。当r n时我们修改的是d[n1]这个位置的值在计算所有i n的a[i]时都用不到所以是安全的。这是一种常见的技巧。修正一下数组长度n4我们开d长度为6。初始d [0, 1, 2, -5, 7, 0](最后一位是0)。 操作d[2] 10- 12d[5] - 10- 7-10-3。现在d [0, 1, 12, -5, -3, 0]。 计算a:a[1] 1a[2] 11213a[3] 112(-5)8a[4] 112(-5)(-3)5还是不对我意识到我犯了一个构建错误。差分数组d[i] a[i] - a[i-1]对于i1a[0]我们视为0。所以d[1] a[1] - 0 1d[2] a[2] - a[1] 3 - 1 2d[3] a[3] - a[2] -2 - 3 -5d[4] a[4] - a[3] 5 - (-2) 7d[5]呢a[5]不存在但我们多开了一位可以认为a[5]0因为我们不关心那么d[5] a[5] - a[4] 0 - 5 -5不不能这样随意定义。更标准的做法是初始构建差分时只构建到d[n]d[n1]初始为0。然后对于区间[l, r]加c的操作我们执行d[l] cd[r1] - c(如果r1 n1因为我们开了n2的空间) 这样当我们通过前缀和还原a[i] sum(d[1..i])时对于i rd[l]的c生效对于i rd[l]的c和d[r1]的-c在求和时抵消。让我们用这个标准流程重做 初始a [0, 1, 3, -2, 5](n4)。 构建差分d长度6d[1]1,d[2]2,d[3]-5,d[4]7,d[5]0,d[6]0。 操作区间[2,4]加10。l2,r4。d[2] 10-d[2]12d[5] - 10-d[5]-10(因为r15) 现在d [0, 1, 12, -5, 7, -10](等等d[4]还是7不对我们只改了d[2]和d[5]d[4]没变还是7。我上面写错了修正d [0, 1, 12, -5, 7, -10]) 现在通过前缀和还原a:a[1] d[1] 1a[2] d[1]d[2] 11213(正确310)a[3] 112(-5)8(正确-210)a[4] 112(-5)715(正确510)完美d[5]的-10在计算a[4]时没有被加上因为i4 5所以不影响。如果我们计算一个虚拟的a[5]它会等于112-57-105相当于在a[4]的基础上又减去了10回到了原始a[4]的值这里有点绕。关键在于我们只关心i从1到n的a[i]。d[n1]及以后的修改不会影响前n项的值。多开的空间就是为了安全地执行d[r1] - c这个操作。3.3 差分的使用流程与还原使用差分数组的典型流程是初始化根据原始数组a构建差分数组d。批量更新进行一系列区间修改操作每次只修改d[l]和d[r1]两个点。最终还原所有更新操作完成后对差分数组d求一次前缀和得到的就是更新后的新数组a。还原操作就是求前缀和# 还原后的数组 a_new 长度 n a_new [0] * (n 1) for i in range(1, n 1): a_new[i] a_new[i-1] d[i] # 或者直接覆盖原数组 a for i in range(1, n 1): a[i] a[i-1] d[i]心得差分的核心是“影响传递”你可以把差分数组d想象成一系列“脉冲”或“指令”。d[l] c表示“从位置l开始后续所有元素都增加c”。d[r1] - c则表示“从位置r1开始取消之前增加c的影响”。两个指令叠加就精确地将影响限制在了区间[l, r]内。这种思想在物理、信号处理等领域非常常见。4. 前缀和与差分的互逆关系及应用场景学到这你应该能清晰地看到前缀和和差分是一对互逆的运算。对数组a求前缀和得到ss[i] sum(a[1..i])。对数组s求差分得到aa[i] s[i] - s[i-1](i1)。同理对数组a求差分得到d对d求前缀和就回到a。这种互逆性意味着它们常常成对出现解决“先更新后查询”或“边更新边查询”的复杂问题。4.1 经典应用场景对比为了更直观我们用一个表格对比它们的典型应用场景特性前缀和 (Prefix Sum)差分 (Difference)核心操作快速查询区间和或满足结合律的运算结果快速进行区间批量修改加/减一个值预处理复杂度O(n)O(n)单次操作复杂度查询O(1)更新O(1)典型问题“静态数组多次区间求和”“多次区间修改最后询问单个元素或整体数组”互逆关系差分数组的前缀和是原数组原数组的差分是差分数组思想延伸二维前缀和求子矩阵和、树上前缀和求路径节点和二维差分子矩阵批量加、树上差分路径节点批量加4.2 结合使用解决“动态区间求和”问题有时候问题会更复杂在一个数组上混合进行两种操作1) 给区间所有数加一个值2) 查询区间和。如果只用前缀和更新太慢只用差分查询需要还原整个数组也慢。这时我们需要更高级的数据结构如树状数组Binary Indexed Tree, BIT或线段树Segment Tree。但有趣的是树状数组的底层思想之一就是前缀和与差分的结合。它通过巧妙的二进制索引既能以 O(log n) 复杂度进行单点更新这可以组合成区间更新也能以 O(log n) 复杂度进行前缀和查询从而得到区间和。学习前缀和与差分是理解这些高级数据结构的重要基石。4.3 从一维到二维思想的自然延伸网络热词中提到了“二维前缀和”这正是一维思想的直接推广。想象一个数字矩阵我们想快速计算任意子矩阵的和。我们可以预处理一个二维前缀和数组s[i][j]表示从(1,1)到(i,j)的矩形内所有元素的和。构建公式s[i][j] a[i][j] s[i-1][j] s[i][j-1] - s[i-1][j-1](加上当前格加上左方矩形前缀和加上上方矩形前缀和减去左上方重复加了一次的矩形)。查询子矩阵(x1,y1)到(x2,y2)的和sum s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] s[x1-1][y1-1]。原理和一维一样是大矩形减去不需要的部分再加回多减的部分。对应的也有二维差分用于快速给一个子矩阵内所有元素加上一个常数。操作的核心依然是“影响传递”只不过在二维中一次区间更新需要在差分矩阵的四个角点上进行修改。4.4 算法之外的联想信号与系统看到热词中的“差分放大电路”、“差分信号”你是否觉得似曾相识在信号处理领域“差分”指的是两个信号之间的差值。差分放大电路放大的是两个输入端的电压差能有效抑制共模噪声两个输入端共有的干扰。这和我们数组中的差分d[i] a[i] - a[i-1]在思想上有异曲同工之妙它关注的是相邻数据的变化量而不是绝对值。这种“关注变化”的思想在数据压缩、边缘检测等领域无处不在。而“前缀和”在离散系统中可以近似看作“积分”的离散形式。微分差分和积分前缀和正是互逆的运算。所以这对概念从数学到算法再到物理电路实现了完美的统一。理解了这个本质再去学习“差分隐私”通过添加可控噪声来保护数据、“差分进化算法”一种基于向量差分的优化算法等概念就会有一种豁然开朗的感觉。5. 实战演练用一道题把思路串起来光说不练假把式。我们来看一道经典题目它完美结合了前缀和与差分的思想。问题描述 有一个长度为n的计数器数组count初始全为0。接下来进行m次操作每次操作给出两个整数l和r(1 l r n)表示将count[l]到count[r]的每个数都加1。请问在所有操作完成后数组count中值最大的那个数是多少并统计有多少个位置的值是这个最大值。输入 第一行两个整数n, m。 接下来m行每行两个整数l, r。输出 两个整数分别表示最大值和达到最大值的元素个数。分析 这就是一个纯粹的区间批量更新问题。最笨的方法是模拟复杂度 O(m*n)肯定超时。 这正是差分的用武之地。我们不需要维护真实的count数组而是维护它的差分数组d。初始化差分数组d长度为n2全部为0因为原数组初始全0所以差分数组自然全0。对于每个操作(l, r)d[l] 1d[r1] - 1所有操作完成后对差分数组d求前缀和得到最终的count数组。遍历count数组找出最大值及其出现次数。代码实现Pythondef solve(): n, m map(int, input().split()) # 差分数组多开两位下标从1开始 diff [0] * (n 2) # 进行m次区间更新操作 for _ in range(m): l, r map(int, input().split()) diff[l] 1 diff[r 1] - 1 # r1 可能等于 n1因为我们开了 n2 的空间所以安全 # 通过前缀和还原最终的count数组并同时找出最大值 max_value 0 max_count 0 current 0 # 当前的前缀和即 count[i] for i in range(1, n 1): current diff[i] # 这就是 count[i] if current max_value: max_value current max_count 1 elif current max_value: max_count 1 print(max_value, max_count) # 调用函数 if __name__ __main__: solve()复杂度分析构建差分和进行m次更新O(m)每次更新是O(1)。还原数组并统计O(n)。总复杂度O(m n)完美。踩坑点数组大小一定要开n2否则d[r1]在rn时会索引越界。初始值因为原数组初始全0所以差分数组初始全0即可。如果原数组有初始值则需要先构建初始的差分数组。同时查询与更新如果题目要求在更新过程中随时查询某个位置的值上述方法就需要在每次查询时计算前缀和效率是O(n)。这就需要用到支持“区间更新、单点查询”的树状数组或线段树了。本题是“先更新完再统一查询”所以差分是最高效的。通过这道题你应该能深刻体会到差分如何将m次“区间遍历加1”的 O(m * 区间长度) 操作优化为m次“修改两个点”的 O(m) 操作。这种优化在数据量大时是决定性的。6. 总结与进阶思考前缀和与差分一查一改相辅相成。它们的强大不在于算法本身有多复杂而在于提供了一种预处理和问题转化的思维范式。这种范式告诉我们面对大量重复性操作时不要急着蛮干先想想能不能通过一次预处理把后续操作的成本降下来。我个人在多次使用中的体会是画图是理解的关键。在纸上画出一个数组手动计算它的前缀和与差分数组再模拟几次区间查询和更新比干看代码有效十倍。下标从1开始能省去大量边界判断让代码更清晰逻辑更一致。这虽然是个小习惯但能显著降低出错概率。一定要测试边界情况l1和rn的情况空区间的情况更新值c为负数的情况等。联想到更广阔的世界一维到二维数组到树“树上差分”解决树上路径更新问题静态到动态引入树状数组/线段树。掌握了基础思想这些扩展都是水到渠成。最后回到网络热词“fdtd时域有限差分 yee网格结构”是计算电磁学中的一种数值方法其核心也是对空间和时间进行差分离散。“差分进化算法”是一种优化算法其“差分”体现在变异操作上通过个体向量之差来产生新个体。你看从算法竞赛到科学计算再到硬件设计“差分”的思想无处不在。所以下次当你遇到需要频繁区间求和或批量更新的问题时别再用那个笨重的循环了。想想前缀和与差分它们可能就是那把隐藏的、能瞬间切开性能瓶颈的利刃。理解它们五分钟或许不够但一旦掌握受益将是长久的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价