一、暴力求和的痛点与前缀和降维
场景:给出一个数组
暴力解法 (for 循环从
物理劣势:我们一直在重复计算。比如刚刚算过
1. 前缀和:提前打包的智慧
为了不重复劳动,我们可以花一点时间做一个“预处理”。
定义一个新数组
它的递推式极其简单:
直观例子:假设原数组 a = [3, -2, 5, 1, 4]。
为了防止边界越界,我们让数组下标从 1 开始,并且强制规定
最终得到的前缀和数组为 s = [0, 3, 1, 6, 7, 11]。
2. 的区间查询:为什么是 l-1?
有了前缀和数组,怎么快速求任意区间
💡 【实战检验】
- 查
[2, 4]:我们想要第 2 到第 4 个数字的和。用(前4个数的总和)减去谁呢?我们想保留第 2 个数字,所以只能把排在它前面的第 1 个数字减掉,也就是减去 。 。核对原数组: ,正确! - 查
[1, 5]:保留第 1 个数字,减去。 。 - 查
[3, 3]:也就是查第 3 个数字本身。。
3. 静态区间求和模板实现
输入约定:第一行读入
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=200005;
int a[N],s[N];
int n,q;
void solve(){
cin>>n>>q;
for(int i=1;i<=n;i++){
cin>>a[i];
// 边读入边计算前缀和,s[0] 默认为 0
s[i]=s[i-1]+a[i];
}
while(q--){
int l,r;
cin>>l>>r;
// O(1) 回答查询
cout<<s[r]-s[l-1]<<'\n';
}
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0);
solve();
return 0;
}
二、一维差分:化批量区间加为两点修改
场景:给出一个数组
暴力解法 (
1. 差分数组的诞生:只记录变化
如果我们不关心每个位置具体是多少,只关心当前元素比前一个元素多了多少,就可以引入差分数组
直观例子:依然是 a = [3, -2, 5, 1, 4]。(假设
得到差分数组 d = [3, -5, 7, -4, 3]。
2. 区间修改的降维打击
现在我们要给区间
- 第
个数字涨了,但它前面的 没涨,所以落差拉大: d[l] += c - 第
个数字没涨,但前面的 涨了,所以落差缩小: d[r+1] -= c
原本要修改
3. 前缀和与差分的逆向关系:原物奉还
当你做完所有乱七八糟的修改后,怎么把原数组还原出来呢?
差分数组的前缀和,就是原数组。
因为
💡 【实战推演】 我们接着用刚才的
d = [3, -5, 7, -4, 3]演示两次操作:操作 1:给
[2, 4]加 3。
d[2] += 3变成 -2;d[5] -= 3变成 0。- 此时差分数组:
[3, -2, 7, -4, 0]。- 如果现在求一遍前缀和还原,原数组变成:
[3, 1, 8, 4, 4]。操作 2:继续给
[3, 5]减 2(即加 -2)。
- 针对当前的差分数组:
d[3] += -2变成 5;d[6] -= -2变成 2(注意越界一位没关系,开够数组即可)。- 此时差分数组:
[3, -2, 5, -4, 0](以及d[6]=2)。- 最终求一遍前缀和还原:
[3, 1, 6, 2, 2]。完美对应了两次操作后的结果!
图中只记录本次修改的增量,差分增量累加后为[0,3,3,3,0],与原数组逐项相加才是修改后的[3,1,8,4,4]。

4. 一维批量区间加模板实现
输入约定:第一行读入
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=200005;
int a[N],d[N];
int n,m;
void solve(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
// 构建初始差分
d[i]=a[i]-a[i-1];
}
// 接收海量修改操作,O(1) 打标记
while(m--){
int l,r,c;
cin>>l>>r>>c;
d[l]+=c;
d[r+1]-=c;
}
// 所有修改结束后,扫一遍前缀和还原
for(int i=1;i<=n;i++){
a[i]=a[i-1]+d[i];
cout<<a[i]<<(i==n?"":" ");
}
cout<<'\n';
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0);
solve();
return 0;
}
三、二维的进阶:容斥原理的重叠抵消
把一维的思想升维到矩阵上,核心逻辑变成了“左边+上边-左上角”。
1. 二维前缀和
递推方程:一块拼图由它左边的拼图、上面的拼图和它自己组成。但左上角被加了两次,必须减去一次。
s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j]
查询区间 ans = s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1]
💡 【矩形推演】 原矩阵:第一行
1 2 3,第二行4 5 6。 按照递推方程,得到的二维前缀和有效部分为: 第一行1 3 6第二行5 12 21
2. 二维差分
在二维空间里,给一个矩形区域加一个数字,只会引起四个角的落差变化。
如果你给左上角
d[x1][y1] += c:整个右下大区域都被加上了。 d[x2+1][y1] -= c:下方的越界区域本不该加,减去。d[x1][y2+1] -= c:右侧的越界区域本不该加,减去。d[x2+1][y2+1] += c:右下角的越界区域被减了两次,补回来。
💡 【批量修改与查询推演】 接着刚才的矩阵。现在我们给“两行中的第2、3列全部加10”。 这相当于给左上角
(1, 2)到右下角(2, 3)的矩形加 10。 修改并还原后,矩阵变为: 第一行1 12 13第二行4 15 16用新前缀和验证:整矩阵总和变成了
。如果只查修改过的第2、3列: 。一切吻合!
3. 二维批量修改后查询模板实现
输入约定:第一行读入行数
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1005;
int a[N][N],d[N][N],s[N][N];
int n,m,u,q;
void solve(){
cin>>n>>m>>u>>q;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
// 初始化构建二维差分
d[i][j]=a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1];
}
}
// O(1) 批量修改四角
while(u--){
int x1,y1,x2,y2,c;
cin>>x1>>y1>>x2>>y2>>c;
d[x1][y1]+=c;
d[x2+1][y1]-=c;
d[x1][y2+1]-=c;
d[x2+1][y2+1]+=c;
}
// O(n*m) 统一还原并计算新前缀和
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
// 差分做前缀和得到原数组,暂存回 a 矩阵
a[i][j]=d[i][j]+a[i-1][j]+a[i][j-1]-a[i-1][j-1];
// 针对新数组再做一次前缀和,用来响应最后的查询
s[i][j]=a[i][j]+s[i-1][j]+s[i][j-1]-s[i-1][j-1];
}
}
// O(1) 响应矩阵和查询
while(q--){
int x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
int ans=s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1];
cout<<ans<<'\n';
}
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0);
solve();
return 0;
}
四、视角的升维:贡献法求所有子段和
如果题目要求计算所有可能非空子段的总和,直接暴力累加是
核心问题:对于数组中的某一个数
我们知道,一个子段由左端点和右端点决定。要让子段包含
- 左端点的选择可以是:
。一共 种选择。 - 右端点的选择可以是:
。一共 种选择。 所以包含 的子段总数就是 。它对总和的贡献就是 。
💡 【实战推演】 原数组
a = [2, -1, 3],长度。
a[1] = 2的出现次数:次。(被包含在 [2],[2,-1],[2,-1,3]中)a[2] = -1的出现次数:次。(包含于 [2,-1],[2,-1,3],[-1],[-1,3])a[3] = 3的出现次数:次。(包含于 [2,-1,3],[-1,3],[3])总贡献 =
。 暴力枚举这 6 个子段的和: 。完全一致,但我们把复杂度降到了 !
五、前缀异或:自反性下的无损抵消
1. 异或运算的物理特性与子段查询
除了加减法,位运算中的**按位异或(^,数学符号
- 任何数异或自己都归零:
(自反性) - 任何数异或 0 保持不变:
(幺元)
我们同样定义前缀异或数组
当我们需要查询区间
为什么直接异或就能抵消?
因为
💡 【手算检验】 设原数组
a = [3, 5, 2, 6](二进制分别为011,101,010,110)。 按照递推求前缀异或:
得到 s = [0, 3, 6, 4, 2]。现在查区间
的异或和: 。 手工验证原数组第 2 到 4 项: 。一步到位!
2. 核心考法:子段异或和为 0 的等价转化
在很多赛题中,题目会问:“有多少个连续子段的异或和为 0?”
直接枚举左右端点是
看懂这个式子,整个问题就瞬间降维了:“找一段异或和为 0 的子区间”,等价于“在前缀异或数组里找两个数值相同的位置”!
我们只需要统计前缀异或数组中每个数字出现了多少次,如果某个数值出现了
六、前缀和与频次统计:负数环境下的子段和计数
1. 为什么负数会让滑动窗口失效?
信奥赛中有一类极为高频的题目:“给出一个数组,求有多少个连续子段的和恰好等于
此时降维的唯一解药,依然是前缀和方程:
这句话的物理意义非常漂亮:
当我们从左到右枚举右端点
2. 状态设计与空前缀 的致命细节
我们一边遍历数组计算当前的前缀和 cnt[val] 记录历史前缀和 val 出现的次数。
对于当前遍历到的位置
- 查询前面有多少个合法的左端点:
ans += cnt[s[r] - k]; - 把当前的前缀和记入桶中:
cnt[s[r]]++。
⚠️ 【考场避坑核心:为什么必须预先执行
cnt[0] = 1?】 十个初学者有九个会漏掉这行代码。请思考: 假设原数组开头的第 1 个数本身就等于,那么子段 的和就是 ,这显然是一个合法解。 此时 , 。我们要找的左端点前缀是 。 这个 对应的是一个“什么数都不选的空前缀”。如果一开始没有把 cnt[0]设为 1,程序就找不到这个合法的起点,所有从下标 1 开始的合法子段会被全部漏算!
💡 【手算推演】 数组
a = [1, -1, 1, 1, -1],求和为的子段个数。 初始: cnt[0] = 1,ans = 0。
:目标 , cnt[0]=1,ans += 1(下标区间[1,1]);记录cnt[1]=1。:目标 , cnt[-1]=0;记录cnt[0]=2。:目标 , cnt[0]=2,ans += 2(下标区间[1,3]与[3,3]);记录cnt[1]=2。:目标 , cnt[1]=2,ans += 2(下标区间[2,4]与[4,4]);记录cnt[2]=1。:目标 , cnt[0]=2,ans += 2(下标区间[1,5]与[3,5]);记录cnt[1]=3。 最终总数:个。完全正确,时间复杂度只有 (用 map)或(用哈希表)。
3. 子段和为 k 计数完整实现
输入约定:第一行包含两个整数
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=200005;
int a[N],s[N];
int n,k;
map<int,int> cnt; // 前缀和数值范围很大且有负数,用 map 统计频次
void solve(){
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
s[i]=s[i-1]+a[i];
}
int ans=0;
// 核心:空前缀 s[0]=0 预先出现了一次
cnt[0]=1;
for(int r=1;r<=n;r++){
int target=s[r]-k;
// 查前面有多少个合法的 l-1 使得 s[l-1] == target
if(cnt.count(target)){
ans+=cnt[target];
}
// 记录当前前缀和,供后面的右端点使用
cnt[s[r]]++;
}
cout<<ans<<'\n';
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0);
solve();
return 0;
}
样例输入:
5 1
1 -1 1 1 -1
样例输出:
7
七、差分到在线树状数组的思维跨越(选学)
📌 【选学先修说明】 本小节属于进阶衔接内容。适合已经学过基础树状数组(Binary Indexed Tree / Fenwick Tree)单点修改与前缀查询的同学阅读。如果尚未接触树状数组,可先体会其中的思维转化路径。
1. 静态离线与动态在线的分水岭
差分数组最迷人的地方在于
如何突破这个死穴?核心钥匙就是:把差分数组搬进树状数组里!
2. 映射一:差分思想驱动“区间加、单点查”
回忆差分的基本定义:差分数组
现在请对比两边的操作对应关系:
- 对原数组做区间修改
加 : 在差分体系下,它只是两次单点修改: 以及 。 - 对原数组做单点查询
: 在差分体系下,它本质上就是求差分数组 的前缀和: 。
树状数组最擅长什么?树状数组天生就是用来做**“单点修改”和“前缀求和”的!
因此,只要我们用一个树状数组去维护差分数组
- 区间修改:调用 2 次树状数组的单点加(位置
加 ,位置 加 ),单次耗时 ; - 单点查询:调用 1 次树状数组的前缀查询(查
的和),单次耗时 。 原本无法承受的动态区间修改与查询,就这样被差分的物理意义轻松攻破!
// 依赖标准树状数组结构:void add(int x, int v) 与 int query(int x)
// 区间加:转化为差分数组的两次单点修改
void range_add(int l, int r, int c){
add(l, c);
add(r + 1, -c);
}
// 单点查:转化为差分数组的前缀求和
int point_query(int x){
return query(x);
}
3. 映射二:两重前缀推导“区间加、区间查”
很多同学会进一步追问:如果不仅要动态区间加,还要动态查任意区间和
这是一块经典的三角形双重求和。让我们从每一个差分元素
存在于 中,一共被加了 次; 存在于 中,一共被加了 次; - 通项规律:
( )一共被累加了 次!
把这个次数代入式子中化简:
极其震撼的结论诞生了:
左边的
这意味着,我们只需要同时开两个树状数组:
- 树状数组
:维护差分数组 ; - 树状数组
:维护辅助数组 。
每次区间加
- 给
维护:位置 加上 ,位置 加上 ; - 给
维护:位置 加上 ,位置 加上 。
任意前缀和
这正是竞赛算法中最迷人的逻辑闭环:前缀和产生差分,差分又反哺前缀和,最终在高级数据结构里融为一体。
八、课堂练习与延伸
- 固定长度最大子段和
给出长度为
的数组,求所有长度为 的连续子段中和最大的是多少?(提示:注意全负数情况下的初始极小值设定,利用前缀和滑动比较)。 - 修改后最大格值与数量 给一个全 0 矩阵做若干次二维区间修改,求最后矩阵里最大的数字是多少,以及这个数字有几个?
- 所有子段总和
不限长度,求一维数组所有可能的子段和加起来是多少?(提示:直接默写上方的贡献法公式,注意数据范围防止乘法爆
long long)。
延伸思考:
基础差分适合“先做完所有修改,再统一进行查询”。如果改成“修改一次,立刻查一次”的交替操作,每次还原重建就要耗费