目录
三、前缀和与差分
3.1 前缀和
3.1.1 一维前缀和的算法思想
3.1.2 一维前缀和的代码实现
3.1.3 二维前缀和的基本思想
3.1.4 二维前缀和的代码实现
3.2 差分
3.2.1 基本思想
3.2.2 代码实现
3.2.3 二维差分的基本思想
3.2.4 二维差分的代码实现
前缀和与差分是一堆逆运算
原数组:一个长度为n的数组{a1, a2, a3 … , an}
前缀和数组:Si = a1+a2+…+ai,S0=0
(第一个下标一定要是1,把S0定义成0,这两个都是为了方便处理边界)
1、前缀和的作用:
能够快速地求出来原数组中一段数据的和,例如,如果想算一个数组中[l,r]的 数组的和,没有前缀和数组的话,复杂度就是O(n)的 ,如果有的话,我们就是可以直接 用 Sr - Sl-1 得到,查询的复杂度就是O(1), 但是求一个前缀和数组的复杂度就是O(n),适用于输入一组数据,让求好几段的数据的和的情况,
2、如何求Si?
( 可以用for循环 递推遍历一遍,S[i] = S[i-1]+ai)
#include
using namespace std;
const int N = 100010;
int q[N];
int s[N];
int main(){
int n, m;
scanf("%d %d", &n, &m);
for(int i = 0; i < n; i++){
scanf("%d", &q[i]);
}
s[0] = 0;
// 构造前缀和数组
for(int i = 1; i <= n;i++){
s[i] = s[i-1] + q[i-1];
}
int l, r;
while(m--){
scanf("%d %d", &l, &r);
printf("%d\n", s[r]-s[l-1]); //区间相减
}
return 0;
}
二位前缀和,画个图 如下所示:
1、如何求某区间的和?
想要求x1, x2, x3, x4所围成的小矩形 的和,就可以用,Sx2y2 - Sx1y2 - Sx2y1 + Sx1y1
2、如何求一个Si j
可以用两层for循环进行遍历,S[i][j] = S[i-1][j] +S[i][j-1] -S[i-1][j-1]+aij
所以定义S0=0,然后第一数都从1开始,就可以避免边界问题
#include
using namespace std;
const int N= 1010;
int q[N][N];
int main(){
int n, m, qn;
scanf("%d %d %d", &n, &m, &qn);
//一定要从1开始,不然边界问题好难处理
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
scanf("%d", &q[i][j]);
}
}
int s[N][N];
// 构建前缀和
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]+q[i][j];
}
}
int xa, ya, xb, yb;
//区间更新
while(qn--){
scanf("%d %d %d %d", &xa, &ya, &xb, &yb);
printf("%d\n", s[xb][yb] - s[xa-1][yb] -s[xb][ya-1] +s[xa-1][ya-1]);
}
return 0;
}
差分是前缀和的逆运算
数组:一个长度为n的数组{a1, a2, a3 … , an}
构造 一个b 数组,{b1,b2,b3…bn}
使得 ai = b1+b2+… +bi
即 b1 = a1; b2= a2-a1; b3 = a3-a2, … , bn = an-an-1
b 就是a 的差分,a就是b 的前缀和
1、差分数组的作用:
也就是在O(n)的时间, 通过 b 差分数组 可以还原出他的 前缀和数组 a ,运用 差分的思想 可以很好的解决一类问题:
例如给定一个区间 [l, r] 让a数组,这里边所有的数都加上c,如果是用遍历,那就需要O(n)的复杂度
如果用差分来做,就可以做到O(1)原理如下:
因为 ai = b1+ b2 + …+ bi
所以只需要对 b[l] 做 +c 的操作,那 a[l] 往后的数都会 +c
又因为题目要求,只是 [l, r] 的区间,所以在对b[r+1] 做-c 的操作,相抵消,那 a[r+1] 之后的数就不会加上c了,
这样再用b数组求前缀和,还原出的a数组 就是最后的结果
所以只需要O(1)的时间就可以实现,以空间换时间
2、如何拥有差分数组呢(初始化)
其实不要考虑 差分数组 的构造,换个角度考虑从0开始用bn 生成an,这样an生成之后,b数组出来了,具体思路如下:
可以先想象a 全部都是0,那么b 也全部都是
然后怎么样变成{a1, a2, a3 … , an}呢
就是相当于 每个地方,进行了加上这个数的操作
就[1,1] 的区间+a1, [2, 2]的区间+a2 …[n, n]的区间+an
这样生成 an 和 完成题目的an 某区间+c 操作,就都可以用同样的代码段了。
#include
using namespace std;
const int N = 100010;
int a[N],b[N];
//在某区间插入某个数,以得到差分数组和前缀和数组
int insert( int l, int r, int c){
b[l] = b[l]+c;
b[r+1] = b[r+1] -c;
}
int main(){
int n, m;
scanf("%d %d", &n, &m);
int c;
//假设本身数组a全是0, 那其差分数组也是全0
//然后一个个给两个数组插入数据,例如在[1,1] 加上a1,调用insert函数
//注意边界,最好从1开始,不然边界超级难处理
for(int i = 1; i <= n; i++){
scanf("%d", &c);
a[i] = c;
insert( i, i, c); //意思就是从i 到i这个区间上的数字 都加上c,
}
int l, r;
//更新区间
while(m--){
scanf("%d %d %d", &l, &r, &c);
insert( l, r, c);
}
//用更新后的b数组还原a数组
for(int i = 1; i <= n; i++){
a[i] = a[i-1] + b[i];
}
//输出最终的数组
for(int i = 1; i <= n; i++){
printf("%d ", a[i]);
}
return 0;
}
思路和一维的是一样的,也不需要考虑直接构造差分数组,而是从0 进行初始化,用bn来生成an,这样b 和 a 数组就都是初始的状态,我们只需要考虑,如何更新:
1、如何更新二维差分数组
如图,在b数组对应的点进行+c 或者 -c的操作,会影响的整个a数组这个点往右及往下的区域都+c或者-c,所以如果只需要对a数组的 x1 x2 y1 y2 围成的矩形中进行+c操作,转化为对b点的操作就是
b[x1][y1] + c; b[x2][y1] - c; b[x1][y2]-c; b[x2][y2]+c
#include
using namespace std;
const int N = 1010; //二维数组的时候,这里不要太大,满足要求就行
int q[N][N], b[N][N];
void insert(int xa, int ya, int xb, int yb, int c){
b[xa][ya] += c;
b[xa][yb+1] -= c;
b[xb+1][ya] -=c;
b[xb+1][yb+1] +=c;
}
int main(){
int n, m, qn;
scanf("%d %d %d", &n, &m, &qn);
//以0开始构建数组
for(int i = 1; i<=n; i++){
for(int j = 1; j<=m; j++){
scanf("%d", &q[i][j]);
insert(i,j,i,j,q[i][j]);
}
}
int xa, ya, xb, yb, c;
//更新区间
while(qn--){
scanf("%d %d %d %d %d", &xa, &ya, &xb, &yb, &c);
insert( xa, ya, xb, yb, c);
}
//生成最后的q数组
for(int i = 1; i<=n; i++){
for(int j = 1; j<=m; j++){
q[i][j] = q[i][j-1]+q[i-1][j]-q[i-1][j-1]+b[i][j];
}
}
//输出最终结果
for(int i = 1; i<=n; i++){
for(int j = 1; j<=m; j++){
printf("%d ", q[i][j]);
}
printf("\n");
}
return 0;
}
写在最后:
这小节所讲的前缀和与差分,更重要的是一种思想,要明白各自适用于解决什么样的问题,可以在后续做题过程中多总结。加油