前缀和和差分其实更像是一种思想
一维前缀和
先来讲前缀和。所谓前缀和,就是前 i 个数的和,用一个数组 s 来记录:
s[i] = a[1]+a[2]+...+a[i](这很像高中数学数列的前n项的和 其实就是)
下面一个前缀和的应用:
输入一个长度为 n 的整数序列
接下来再输入 m 个询问,每个询问输入一对 l,r 区间(l,r>=1)
对于每个询问,输出原序列中从第 l 个数到第 r 个数的和
下面是实现代码(为了方便看懂,我使用了两个数组,实际上这个问题只要一个就好了)
#include<iostream>
using namespace std;
const int N=1000;
int a[N],s[N];
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
//输入n个数,查询m次索引区间[l,r]的和(l>=0,r>=0)
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];
//也可以直接采用一个数组就实现前缀和 a[i]= a[i-1]+ a[i]; 前提是原数组不再被使用了
}
while(m--){
int l, r; //l,r均为一段1-base区间
cin>>l >>r;
cout<< s[r]-s[l-1] << '\n';
}
}
依旧解释核心部分
核心代码
for(int i=1; i<=n; ++i){//这里就是在实现前缀和
s[i]= s[i-1]+ a[i];
}
s[i]的含义就是前i个数的和(前缀和)
这里有个建议:下标从 1 开始存储数组(1-base),这样 s[0] 自然就是 0,查询 [1,r] 的时候当 l = 1 的时候不会越界也不用特判边界
cout<< s[r]- s[l-1];
前缀和最经典的应用:O(1) 查询区间和。s[r] 是前 r 个数的和,
s[l-1] 是前 l-1 个数的和,两者的差s[r]-s[l-1] 也就是中间区间 [l,r] 这段的和就出来了
举个例子,a=[1,2,3,4,5]:
s[1]=1, s[2]=3, s[3]=6, s[4]=10, s[5]=15
查询 [2,4] 的和:s[4]-s[1]=10-1=9,即 2+3+4=9
一维差分
接下来是差分。差分就是前缀和的逆运算:前缀和是"前n个累加",差分是"两两做差"
差分数组,原数组,前缀和数组有一个很特别的关系
差分 差分
差分数组 <---------- 原数组 <---------- 前缀和数组
----------> ---------->
前缀和 前缀和
下面便有一个体现两者关系的题目:
输入一个长度为 n的整数序列
接下来输入 m 个操作,每个操作包含三个整数 l, r, c,表示将序列中 [l,r] 之间的每个数加上 c
请你输出进行完所有操作后的序列。
这个题传统的暴力确实可以解,但是时间复杂度就不乐观了,下面来看怎么用差分实现:
#include<iostream>
using namespace std;
const int N= 100010;
int a[N],d[N];
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){
d[i]= a[i]-a[i-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){
d[i]+=d[i-1]; // 进行一次前缀和,回到修改后的数组
cout<< d[i]<<' ';
}
//--------------------核心区分割线---------------------
return 0;
}
我们来解释一下核心区域
核心代码
d[i]= a[i]- a[i-1];
这里是差分数组的定义:相邻两个数两两作差,a[i]- a[i-1]
而 差分+前缀和 可以把 "区间修改" 变成 "单点修改",这也是差分+前缀和的很常见的一个应用:
先对差分数组的如下地方进行操作
d[l] += c; // 在 l 处加上c
d[r+1] -= c; // 在 r+1 处减去c,在计算前缀和的时候可以防止对 r 位置后的数字造成影响
然后,再进行一次前缀和的操作:
for(int i=1; i<=n; ++i){
d[i] += d[i-1]; // 进行一次前缀和,回到修改后的数组
cout<< d[i]<<' ';
}
这样得到的结果就是对特定区间操作后的原数组了,就不用来一次嵌套for循环了
为什么?直接举例子:a=[a1, a2, a3, a4, a5],想要给 [a2,a4] 加上c
- 第一步,得到差分数组
d=[a1, a2-a1, a3-a2, a4-a3, a5-a4] - 第二步
d[l] += c;d[r+1] -= c;得到d=[a1, a2-a1+c, a3-a2+c, a4-a3+c, a5-a4-c] - 最后,按照前缀和操作,最后结果为:
d=[a1, a2+c, a3+c, a4+c, a5]
二维前缀和
我们先来看一个问题
给出一个点阵,需要你计算某个以 (x1, y1) 为左上角,(x2, y2)为右下角的点阵区域的和
这个问题就是二维前缀和的应用
我们先来给出二维前缀和的几何意义(很重要):
s[i][j]代表着这个点与坐标原点围成的矩形区域内(包含边界)所有点的权重之和
下面我们来看代码实现:
#include<iostream>
using namespace std;
const int N= 1010;
int a[N][N], s[N][N];
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]+ a[i][j]-s[i-1][j-1];
}
}
// 子矩阵的和
while(q--){
// 均为1-base下的索引坐标
int x1, y1, x2, y2;
cin>> x1>> y1>> x2>> y2;
cout<< s[x2][y2]- s[x1-1][y2]- s[x2][y1-1]+ s[x1-1][y1-1]<<'\n';
}
//--------------------核心区分割线---------------------
return 0;
}
核心代码
前缀和一共有两个公式:
s[i][j]= s[i-1][j]+ s[i][j-1]+ a[i][j]-s[i-1][j-1]s[x2][y2]- s[x1-1][y2]- s[x2][y1-1]+ s[x1-1][y1-1]
第一个公式用来计算s[i][j]的值,还记的这个的几何意义吗?s[i][j]代表着这个点与坐标原点围成的区域内(包含边界)所有点的数值之和
第二个公式用来计算以 (x1, y1) 为左上角,(x2, y2)为右下角的点阵区域的和
这两个公式如果自己画一个点阵图,根据s[i][j]的含义就可以很快理解了
差分
说实话,差分这里不方便单独拿出来讲,我觉得配合前缀和一起讲更好
二维差分+前缀和主要解决的是这样一个问题:
给二维点阵某个区域的点全部加上一个数
下面我们来看看代码实现:
#include<iostream>
using namespace std;
const int N= 10010;
int a[N][N], d[N][N];
// 配合前缀和,可以让修改可以形成一个平面范围的影响
// 你可以自己画一个点阵,左上角为原点
// 然后这段代码配合最后的前缀和的几何意义就是:
// 以坐标为左上角的点的矩形,区域内的所有点都加上c
void dif(int x1, int y1, int x2, int y2, int c){
d[x1][y1]+= c;
d[x1][y2+1]-= c;
d[x2+1][y1]-= c;
d[x2+1][y2+1]+= c;
}
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];
}
}
//--------------------核心区分割线---------------------
// 这里并不是直接采用公式构造,要注意了
// 这种写法和使用公式构造差分数组的效果是一样的
// 差分公式:d[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1]
for(int i=1;i<=n; ++i){
for(int j=1; j<=m; ++j){
dif(i, j, i, j, a[i][j]);
}
}
// 指定子矩阵区域加上c
while(q--){
int x1,y1,x2,y2,c;
cin>> x1>> y1>> x2>> y2 >>c;
dif(x1, y1, x2, y2, c);
}
for(int i=1;i<=n; ++i){
for(int j=1; j<=m; ++j){
d[i][j]+= d[i-1][j]+ d[i][j-1]- d[i-1][j-1]; // 前缀和
}
}
//--------------------核心区分割线---------------------
for(int i=1;i<=n; ++i){
for(int j=1; j<=m; ++j){
cout<< d[i][j] <<' ';
}
cout<<'\n';
}
}
核心代码
二维差分公式是这个:
d[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1]
但是,这里我们使用了另外一个办法来实现差分数组的创建:
先定义一个dif函数
void dif(int x1, int y1, int x2, int y2, int c){
d[x1][y1]+= c;
d[x1][y2+1]-= c;
d[x2+1][y1]-= c;
d[x2+1][y2+1]+= c;
}
然后这样子写:
// 这里并不是直接采用公式构造,要注意了
// 这种写法和使用公式构造差分数组的效果是一样的
for(int i=1;i<=n; ++i){
for(int j=1; j<=m; ++j){
dif(i, j, i, j, a[i][j]);
}
}
这样子得到的差分数组和使用公式构造是一个效果的
**可是我们为什么要这样写?**原因:
这个函数配合前缀和,可以让修改可以形成一个平面范围的影响
你可以自己画一个点阵,左上角为原点,然后这段代码配合最后的前缀和的几何意义就是:
以坐标为左上角的点的矩形,区域内的所有点都加上c
这一点很重要,直接影响到我们后续理解,下面这个代码就是对这个意义的应用:
while(q--){
int x1,y1,x2,y2,c;
cin>> x1>> y1>> x2>> y2 >>c;
dif(x1, y1, x2, y2, c);
}
for(int i=1;i<=n; ++i){
for(int j=1; j<=m; ++j){
d[i][j]+= d[i-1][j]+ d[i][j-1]- d[i-1][j-1]; // 前缀和
}
}
还是那句话,可以自己画一个点阵图来理解
评论