前缀和与差分
一、一维前缀和
1. 算法介绍
前缀和本身严格来说并不能说是一个算法,它更像是一种 工具 ,帮助我们解题。
类比的说,前缀和,与我们中学所学知识里数组中的 前n项和Sn 的概念相近。计算前缀和本身并不能帮我们解决问题,而通过计算前缀和却可以帮助我们更快、更好的完成问题的解决。
2. 算法思想
前缀和的创建
假如a[N]
为待求前缀和的数组,sum[N]
为前缀和数组,我们通过计算使得sum[N]
中任意元素sum[i]
是a[N]
数组下标从0
到i
元素的和,即sum[i] = a[0] + a[1] + ······+a[i]
。
不难发现,我们可以通过公式 sum[i] = sum[ i - 1] + a[i]
来计算数组b[N]
的值。
值得注意的是,为了防止当i = 0
时,数组元素i - 1 = 0 ,使得 sum[ i - 1]
越界,故我们在设计数组a[N],sum[N]
时,数组下标从 1 开始置入数字元素。
前缀和的运用
前缀和的元素是就是用来计算 0 , r
范围内的数组元素的和,而利用前缀和,我们便可以通过公式:
从而计算l , r
范围内的数组元素的和,正如上例所述。
3. 算法模板
S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]
解释如上,不再赘述。
4. 算法实现
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int n , m ;
int q[N],sum[N];
int main(){
scanf("%d %d",&n,&m);
for(int i = 1 ; i <= n ; i ++) //注意数组下标从1开始
scanf("%d",&q[i]);
for(int i = 1 ; i <= n ; i ++)
sum[i] = sum[i - 1] + q[i];
while( m-- ){
int l , r ;
scanf("%d %d", &l , &r );
printf("%d\n",sum[r] - sum[ l - 1]);
}
return 0;
}
二、二维前缀和
1. 算法介绍
一维前缀和是建立于一维数组上的,那么二维前缀和就不难联想到建立在二维数组上。
类比的来看,一维前缀和方便于计算一维数组中从l到r
元素的和,那么二维前缀和便有利于计算二维数组中:
从(x1 , y 1)到(x2 , y 2)元素的和。
2. 算法思想
二维前缀和的创建
二维前缀和数组sum[N][N]
,其实就是建立一个数组,其中的元素的值,为待求数组a[N][N]
从a[1][1]
到a[i][j]
之和。故建立二维前缀和数组的方式与一维前缀和相类似,只需要令:
sum[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j]
即可求出二维前缀和。
是不是觉得有些突兀?没关系,下面这个图来帮助你理解。
-
首先,
sum[i][j]
就是将如下图红框内的元素之和放入其中。
-
应该如何实现呢?我们现在已知的其实是
a[i][j]
,那么先求出在sum[i][j]
以内a[i][j]
以外的面积,然后再加上a[i][j]
就好了。 -
问题在于
sum[i][j]
以内a[i][j]
以外的面积呢?如下图所示,我们可以利用粉色部分和绿色部分,即
s[i-1][j]+s[i][j-1]
覆盖在sum[i][j]
以内a[i][j]
以外的部分。但细心的同学已经发现,粉色和绿色两个颜色相加时,多加了一次其二者重叠的棕色部分,无碍,只需要再减去这部分的面积即可求出所想得到的面积了,也就是上面的公式了。
二维前缀和的应用
引言
好吧,我们费尽心思求出来的这个二位前缀和数组究竟有什么用呢?
其实很简单,与一位前缀和类似,就是方便计算从(x1,y1)到(x2,y2)
二维数组的元素之和。
或许你又想说,那我遍历一遍不就求出来了?搁你这么麻烦呢?
其实,当只有一组求和时,确实二者相差并不大,有点脱xx放x的意思。但是当我们有m组查询,而m趋于无 限的时候,该应用可以将时间复杂度从O(n * m)降到 O(n + m),这可是一笔不少的性能提升。
核心
那么如何运用二维前缀和求出从(x1,y1)到(x2,y2)
二维数组的元素之和呢?
很简单,类似于创建,我们只需要利用公式:
sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y - 1] + sum[x1 - 1][y1 - 1]
就能很轻松的算出所求的面积了!啥?你问我咋来的?自己想去吧!
3. 算法模板
//Sum[i, j] = 第i行j列格子左上部分所有元素的和
//以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
Sum[x2, y2] - Sum[x1 - 1, y2] - Sum[x2, y1 - 1] + Sum[x1 - 1, y1 - 1]
4. 算法实现
#include <iostream>
using namespace std;
const int N = 1010;
int n , m , q;
int a[N][N] , sum[N][N];
int main(){
scanf(" %d %d %d", &n , &m ,&q );
for(int i = 1 ; i <= n ; i++)
for(int j = 1 ; j <= m ; j++)
scanf("%d", &a[i][j]);
for(int i = 1 ; i <= n ; i++)
for(int j = 1 ; j <= m ; j++)
sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i - 1][j - 1] + a[i][j];
while(q--){
int x1 , y1 , x2 , y2;
scanf("%d %d %d %d", &x1 , &y1 , &x2 , &y2);
printf("%d\n",sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][ y1 - 1] + sum[x1 - 1][y1 - 1]);
}
return 0 ;
}
三、一维差分
1. 算法介绍
一维差分,是一维前缀和的逆运算。后者是创建一个数组sum[N]
,使其为数组a[N]
的前n项和;而后者,则是创建一个数组d[N]
,使其的前n项和为a[N]
。
一维差分同一位前缀和一样,也是用来解决某种问题的工具。
思考这样一个问题,对于一个给定数组a[N]
,将序列中 [l,r]
之间的每个数加上 一个常数c
。
我们通常的做法是,遍历一次数组,将[l,c]
的元素依次都加上c
;而一维差分将提供一个在修改次数较多时能够较快完成该操作的方法。
2. 算法思想
一维差分的创建
不同于一维前缀和,我们并不能通过已知数组直接获得所想数组。(一维前缀和,只需将现有数组的前n项和相加即可;而一维差分,却并不清楚具体是哪两个数的和能够等于已知数列元素)
不过,我们可以假想一个数组d[N]
,使其满足数组d[i]
的前i
项和为a[i]
。
通过建立该假想,我们不难发现:
可以借此公式,得出数组d[N]
。值得注意的是,在求一维差分时,数组下标也需要从1
开始。
一维差分的应用
正如算法介绍中的问题所述,我们可以先让d[l]
上的元素加上c
,即d[l] + c
。利用此时的d[N]
数组通过求出前缀和可知:新的数组sum[N]
从l
元素之后的所有项都比a[N]
中的项大c
。
因此,我们需要再为d[l]
数组打个补丁——让d[r + 1]
上的元素再减去c
。如此,新的数组sum[N]
只有在从l到r
的数组元素比a[N]
大c
,这正是我们想要求得的。
综上所述,其公式应为:
3. 算法模板
给区间[l, r]中的每个数加上c:d[l] += c, d[r + 1] -= c
4. 算法实现
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int a[N] , d[N];
int n , m;
int main(){
scanf("%d %d", &n , &m);
for(int i = 1 ; i <= n ; i++)
scanf("%d", &a[i]);
for(int i = 1 ; i <= n ; i++)
d[i] = a[i] - a[i - 1];
while(m--){
int l , r , c;
scanf("%d %d %d", &l , &r , &c);
d[l] += c;
d[r + 1] -= c;
}
for(int i = 1 ; i <= n ; i++)
a[i] = a[i - 1] + d[i];
for(int i = 1 ; i <= n ; i++)
printf("%d ",a[i]);
}
四、二维差分
1. 算法介绍
相信在前面的讲解后,你已经能够隐约猜到二维差分的实现作用了。没错,二位差分就是用于解决给(x1,y1)和(x2,y2)为边界的子矩阵元素均加上c
的问题了。但是,二维差分的实现和思想可以说是这四种方法中最为麻烦的一种,好在,虽然麻烦,但并不复杂。
2. 算法思想
二维差分的创建
假设有两个数组 a[N][N],d[N][N]
,其中,a[N][N]
数组是d[N][N]
数组的二维前缀和。
现在已知数组a[N][N]
,那么也就是根据二位前缀和数组来求二维差分d[N][N]
。
那么如何解决这个问题?容我先卖个关子,不如我们先来看看二维差分的应用。
二维差分的应用
假设现在已知d[N][N]
数组,那么当改变d[i][j]
元素时,将会影响到数组a[N][N]
里的从(i,j)
之后的所有元素。这点与一维差分类似。
那么如何利用差分思想解决子矩阵元素加上c
的问题呢?其公式为:
是否觉得有点突兀?没关系,可以通过下面的图来理解:
d[x1][y1] += c
,此步过后,数组sum[N][N]
从(x1,y1)
之后的所有元素均被加上了c
;
d[x2+1][y1] -= c
,此步过后数组sum[N][N]
中紫色区域的元素均被减去了c
,与数组a[N][N]
保持一致。
d[x1][y2+1] -= c
,此步过后数组sum[N][N]
中绿色区域的元素均被减去了c
,与数组a[N][N]
保持一致。
d[x2+1][y2+1] += c
,不难发现,上面两步重复对红色区域的元素减去了c
,也就是说,对于红色区域的元素此时sum[i][j] = a[i][j] - c
,该步则需要补回这部分c
。
简记:
在编写该算法时,可以脑中先回想其对应的图片,随后分清楚,对其进行操作,需要在何处开始加上c
,并需要在何处打上补丁c
,随后,中间两步则是将需要打补丁的横纵坐标 结合 开始操作处的横纵坐标 结合填入减去c
。
我们在对于二维差分的应用时,就是对于在(x1,y1)到(x2,y2)范围内
插入一个c
。
那么我们换一个角度,在创建二维差分时,是不是可以将其看作是从(i,j)到(i,j)范围内
插入一个a[i][j]
呢?
3. 算法模板
给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c
4. 算法实现
#include<iostream>
using namespace std;
const int N = 1e3 + 10;
int a[N][N] , d[N][N];
int n , m , q;
void Insert(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(){
scanf("%d %d %d", &n , &m , &q);
for(int i = 1 ; i <= n ; i++)
for(int j = 1 ; j <= m ; j++)
scanf("%d" ,&a[i][j]);
for(int i = 1 ; i <= n ; i++)
for(int j = 1 ; j <= m ; j++)
Insert( i , j , i , j , a[i][j]);
while(q--){
int x1 , y1 , x2 , y2 , c;
scanf("%d %d %d %d %d",&x1, &y1, &x2 ,&y2 ,&c);
Insert(x1,y1,x2,y2,c);
}
for(int i = 1 ; i <= n ; i++)
for(int j = 1 ; j <= m ; j++)
a[i][j] = a[i - 1][j] + a[i][j - 1] -a[i - 1][ j - 1 ] + d[i][j];
for(int i = 1 ; i <= n ; i++){
for(int j = 1 ; j <= m ; j++){
printf("%d " , a[i][j]);
}
printf("\n");
}
return 0;
}