刷题久了你会发现,很多看起来不难的问题最后都卡在“反复问某个范围内的总和”上。而只要提到这类题,就绕不开前缀和、差分、差分矩阵这套非常经典的技巧;进一步说,一旦情景从一维数组变成二维矩阵,比如算一个子矩阵里所有数字的和,又变成了二维前缀和的舞台。如果有人问我在算法竞赛、面试笔试里最优先要掌握的“性价比”算法是什么,我大概率会推荐这一组:前缀和、子矩阵和、差分、差分矩阵。
它们没有什么高深数学门槛,几行递推就能写出来,却能把很多看起来会超时的暴力循环直接优化一个量级。我刚开始打比赛的时候,习惯一股脑地对每个区间重新累加,结果提交后超时卡到怀疑人生。后来认真把“前缀和”和“差分”这套互为逆向的工具放到一起整理,才发现它们的组合能解决一大批区间静态查询、区间批量更新问题。这篇文章不为讲花哨的高阶技巧,就踏踏实实把这四个概念拆开,配合思路分析和调试记忆,让刚接触的人也能把它们写成自己的模板。
1. 从一个老问题聊起:区间求和的暴力做法到底慢在哪
先看一个特别常见的题目描述:给定一个长度为 n 的数组,多次询问某个区间 [l, r] 内所有元素的和,n 和询问次数都可能到达 1e5 甚至 1e6。最容易想到的办法是每次询问都用一个循环从 l 累加到 r。这个循环看起来很简单,但实际运算量大约是询问次数乘区间平均长度。如果 n 和 q 都是 1e5,最坏情况下要做 1e10 次加法,在竞赛环境中基本会超时。
这里涉及到一个时间复杂度估算的问题。很多人一开始没有估算意识,觉得“每次累加,最多几万次,应该没问题”,可一旦数据规模到了 1e5 以上,朴素版本的代价就藏不住了。现场被打回提交后,最朴素的优化思路就是:能不能提前把“从起点到当前位置的和”记下来?同样的场景放到生活里,相当于你有一本账本,如果每次想知道某几天一共花了多少钱,你都从头加一遍,那会很慢。更好的做法是一开始就维护一个“累计金额”列表,第 i 天存的是从第 1 天到第 i 天的累计花费。
所以,程序里的前缀和数组 pre[i] 就用来记录原数组 a[1] 到 a[i] 的累加和。有了这张表,任意区间 [l, r] 的和就不再需要循环累加,直接通过 pre[r] - pre[l-1] 一步算出来。这样构造前缀和数组的时间是 O(n),之后每次查询是 O(1),整体从 O(nq) 降成 O(n+q),效果会非常明显。在我看来,前缀和最核心的思想就是把“重复计算”变成“预先计算”,这是所有区间类问题里非常值得记住的优化逻辑。
1.1 前缀和数组的定义与递推式
写代码之前先把定义说清楚。为了处理边界方便,我习惯让数组从下标 1 开始使用,并把 pre[0] 设置为 0。对于原数组 a[1..n],定义:
pre[i] = pre[i - 1] + a[i]
如果要查询 a[l] 加到 a[r] 的和,直接用:
sum(l, r) = pre[r] - pre[l - 1]
为什么是 pre[l - 1] 而不是 pre[l]?因为 pre[r] 里面已经包含了 a[1] 到 a[r] 的总和,pre[l - 1] 是 a[1] 到 a[l - 1] 的总和。两者做差,a[1] 到 a[l-1] 这一整段被抵消,剩下的正好是 a[l] + ... + a[r]。边界处理上,如果 l = 1,那么 pre[0] = 0,pre[r] - pre[0] 也依然正确。这一招在很多竞赛模板里都能直接套用。
构造的过程写成代码也特别短:
cpp复制int n;
vector<long long> a(n + 1), pre(n + 1, 0);
for (int i = 1; i <= n; ++i) {
pre[i] = pre[i - 1] + a[i];
}
查询的时候这样写:
cpp复制long long range_sum = pre[r] - pre[l - 1];
1.2 从一维到“能不能更通用一点”
有的读者可能觉得,这只是从“每次 O(n)”变成“预处理 O(n) + 查询 O(1)”,好像不算特别惊艳。但前缀和的真正威力在于它作为一种“预处理工具”,可以和其他技巧嵌套使用。比如配合二分,可以在排序后快速判断满足条件的区间;配合哈希表,可以统计和为某值的子数组数量;再往深走一点,还有二维前缀和、树上前缀和、差分前缀和等一堆延伸。
我在实际做题时总结过一个选择信号:如果题目给的数组是静态的,所有操作都是查询,没有任何中间修改,那一旦出现“多次问某个区间/某个区域的数字和”,第一反应就应该是前缀和。为什么强调静态?因为要是中间频繁修改数组元素并继续查询,普通前缀和每次都要重新构造,就不划算了,此时要换线段树或树状数组的思路。理解前缀和的适用边界,比单纯记住公式要重要得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 差分:区间统一增减的“反向操作”
把前缀和弄明白之后,差分学起来会快很多。从数学上看,差分数组和前缘和数组是一对“互逆关系”:如果 pre 是 a 的前缀和,那么 a 也可以认为是 pre 的差分。差分记录的不是“当前和”,而是“当前位置相对前一个位置的变化量”。
为什么要记录变化量?因为现实里我们会遇到另一类题目:给定一个数组,进行非常多次区间操作,每次把 [l, r] 内的所有数统一加 c,等所有操作全部结束之后,再输出最终的数组。如果每次操作都去 for 循环更新区间内的每个元素,一次区间操作就是 O(n),多次操作之后复杂度爆炸。差分数组能把这个区间更新的过程压缩成两次单点操作,让每次操作的代价降为 O(1)。
2.1 差分数组的定义
假设有一个长度为 n 的数组 a,定义
