前缀和与差分

Posted 0x3f3f3f3f

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前缀和与差分相关的知识,希望对你有一定的参考价值。

前缀和简介

前缀和严格意义上来讲并不是一种算法,而是一种数学思想,其作用是可以以$O(1)$的时间复杂度求出一段区间的和。

一维前缀和

前缀和定义:
设原序列为:$a[1],a[2],a[3],a[4]....a[n]$
则前缀和序列s为
$s[1] = a[1]$
$s[2] = a[1] + a[2]$
$s[3] = a[1] + a[2] + a[3]$
$s[n] = a[1] + a[2] + .... + a[n]$
如何求前缀和数组
直接用for循环递推来求
```c++
for (int i = 1; i <= n; i ++)
s[i] = s[i - 1] + a[i];

注意边界情况:
一般情况下,前缀和的下标要从1开始,并且要将$s[0]$定义成0(原因见下一条)
**前缀和数组的作用**
可以快速求出原数组一段区间的和
假设我们要求原数组$[l,r]$这段的和
如果暴力来做for循环一遍的时间复杂度为$O(n)$
但是如果用前缀和数组可以在$O(1)$的时间复杂度内求出区间和
通过公式$sum[l,r]=s[r]-s[l-1]$
**证明:**
$sum[l,r]=a[l]+a[l+1]+...+a[r]$
因为$s[l - 1] = a[1] + a[2] + ... + a[l - 1]$
又因为$s[r] = a[1] + a[2] + ... + a[l-1] + a[l] + ... + a[r]$
可以看到$s[r] - s[l-1] = a[l] + ....+a[r]$即为$sum$的值
**上述边界问题的原因**
如果我们要求$[1,10]$的前缀和,按照公式应该是$s[10]-s[0]$
因为这个和应该是$a[1] + a[2] + ... + a[10]$正好是$s[10]$的值
因此可以得到$s[0]=0$
所以前缀和要从1开始算(防止相减时越界),且要把0位置初始化成0(避免处理特殊情况)
**一维前缀和模板**
```c++
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int a[N], s[N];
int main()

    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]; // 前缀和的初始化

    while (m -- )
    
        int l, r;
        cin >> l >> r;
        cout << s[r] - s[l - 1] << endl; // 区间和的计算
    
    return 0;

二维前缀和

一维前缀和是求出一段区间的和,同样的,我们可以将这种思想扩展到二维上,也就是求一个子矩阵的和。
二维前缀和指的是,以(x,y)为右下角端点的,所有其左上方元素的和,如下图所示,红线区域所有元素的和为$s[x][y]$的值

如何求二维前缀和
如下图

设原序列为$a[][]$,前缀和序列为$s[][]$
则$s[i][j]$的值等价于,红色框框括起来的区域加上黑色框框括起来的区域,此时,图中灰色部分,被加了两次,因此要减去一次灰色区域,最后加上$a[i][j]$这个点本身,我们根据二维前缀和的定义可以见得,红色框框括起来的区域就是下标为$(i, j-1)$对应的前缀和,同理黑色框框括起来的区域为下标为$(i-1,j)$对应的前缀和,而灰色区域则是下标为$(i-1,j-1)$对应的前缀和
所以初始化二维前缀和的操作为
```c++
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];

**如何求子矩阵的和**
子矩阵的和指的是,以(x2,y2)为右下角坐标,(x1,y1)为左上角坐标所圈起来的矩阵的和
去下图所示灰色区域为待求的子矩阵
![前缀和.png](https://s2.51cto.com/images/20220227/1645971021926305.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)
子矩阵的和求法如下图
![前缀和.png](https://s2.51cto.com/images/20220227/1645971167245016.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)
灰色区域的和为,以$(x2,y2)$为坐标的矩阵的前缀和,减去绿色框框所括起来的和,再减去黑色框框括起来的和,此时图中紫色区域被减了两次,因此要加上图中紫色区域的和,此时为图中灰色区域的和。其中绿色框框对应的区域为$s[x2][y1-1]$的值,黑色区域为$s[x1-1][y2]$的值,紫色区域为$s[x1-1][y1-1]$对应的值
所以求二维前缀和子矩阵的和的求法为
```c++
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];

差分简介

差分是前缀和的逆运算。差分是一种处理数据的巧妙而简单的方法,它应用于区间的修改和询问问题。把给定的数据元素集A分成很多区间,对这些区间做很多次操作,每次操作是对某个区间内的所有元素做相同的加减操作,若一个个地修改这个区间内的每个元素,非常耗时。时间复杂度为$O(nk)$其中$k$为操作次数
因此我们要引入差分数组B,当修改某个区间时,只需要对差分数组做若干操作,就能影响到原来区间的修改,而且操作非常容易,是 $O(1)$ 复杂度的。当所有的修改操作结束后,再利用差分数组,计算出新的前缀和数组即操作后的序列。

一维差分

一维差分定义:
我们设原序列为$a[]$,差分序列为$b[]$
原序列:$a[1],a[2],a[3], ...,a[n]$
构造:$b[1],b[2],b[3],....b[n]$
使得
$a[i] = b[1] + b[2] + ... + b[i]$
这时我们叫做$b$为$a$的差分,$a$为$b$的前缀和
所以只要我们有$b$数组,就可以在$O(n)$的复杂度下求出$a$数组,即求前缀和操作
所以我们可以很容易得到
$b[1] = a[1]$
$b[2] = a[2] - a[1]$
$b[3] = a[3] -a[2]$
$b[n] = b[n] - b[n -1]$
一维差分解决的问题
题目会给我们一堆操作,让我们在$a[l - r]$上都加上或者减去一个常数$c$,问我们若干次操作之后的序列
如果暴力来做,单次操作的时间复杂度无疑是$O(n)$的,则这时我们就要构造差分数组来求,差分操作的复杂度为$O(1)$
操作为
$b[l] + c,b[r+1]-c$
因为如果$b[l]$加上$c$,则求前缀和序列时,因为$a[l]$,以及$a[l]$后边的所有前缀和都要算$b[l]$这个点,因此相当于把前缀和数组的$a[l] + c, a[l + 1] + c ...a[r] + c, a[r + 1] + c.... a[n] + c$
而$b[r + 1] - c$,则求前缀和序列时,同理从$a[r + 1]$后的每一个元素都要减去$c$,即$a[r + 1] -c,a[r + 2] -c, ..... a[n] - c$。
这样这两个操作以后就相当于给$a[l] + c, a[l + 1] + c ,....., a[r] + c$,即为题目所求
一维差分的构造
最开始我们可以假设$a$数组全为0,则差分数组$b$也全为0,但是此时$a$数组有值,我们可以看成是进行了$n$次修改操作
第一次是在$[1,1]$区间加上$a[1]$
第二次是在$[2,2]$区间加上$a[2]$
一直到在$[n,n]$区间加上$a[n]$
证明:
如果我们假设给$[1,1]$区间加上$a[1]$,相当于$b[1] + a[1], b[2] - a[1]$
给$[2,2]$区间加上$a[2]$,相当于$b[2] + a[2], b[3] - a[2]$
给$[3,3]$区间加上$a[3]$,相当于$b[3] + a[3], b[4] - a[3]$
上述操作结束后我们可以看到
b[1] = a[1]
b[2] = a[2] - a[1]
b[3] = a[3] - a[2]
符合差分数组的构造,因此经过n次操作后,即可得到差分数组
差分模板
```c++
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int a[N], b[N];
void insert(int l, int r, int c)

b[l] += c;
b[r + 1] -= c;

int main()

cin >> n >> m;
for (int i = 1; i <= n; i ++ )
cin >> a[i];
for (int i = 1; i <= n; i ++ )
insert(i, i, a[i]);
while (m -- )

int l, r, c;
cin >> l >> r >> c;
insert(l, r, c);

for (int i = 1; i <= n; i ++ )
b[i] += b[i - 1];
for (int i = 1; i <= n; i ++ )
cout << b[i] << " ";
return 0;


### 二维差分
同样的,这里的二维差分我们可以类比二维前缀和,就是已知一个矩阵$a[][]$,我们要构造一个差分矩阵$b[][]$,使得$a[i][j]$,存的是所有$b[i][j]$左上方矩阵内所有元素的和。即$a$矩阵是$b$矩阵的前缀和矩阵。同样的二维差分的构造也可以类比一维差分,首先假设可以看成a矩阵内元素全为0,则b矩阵内元素也全为0,这样如果a矩阵内元素不为0了,可以看做是在$(i, j, i, j)$插入一个元素$a[i][j]$,由于二维差分构造证明赘述过于麻烦,证明可以自己类比一维差分的构造去写。
**二维差分解决的问题**
给以$(x1,y1)$为左上角点的坐标,以$(x2,y2)$为右下角点的坐标的子矩阵里的每一个元素都加上或者减去一个常数$c$。
如果暴力来做的话,因为要循环整个矩阵里的元素,因此时间复杂度为$O(n^2)$。
但是如果构造差分矩阵来做的话,就可以在$O(1)$的时间复杂度内给子矩阵加上一个常数。然后再求出原矩阵即为答案(做二维前缀和操作)。
**操作为**
```c++
  //其中x1,y1为左上角点的坐标,x2,y2为右下角点的坐标
  b[x1][y1] += c;
  b[x2 + 1][y1] -= c;
  b[x1][y2 + 1] -= c;
  b[x2 + 1][y2 + 1] += c;

证明
如下图所示

如果我们给$b[x1][y1]$都加上一个常数$c$的话,那么这个点右下角所有的元素,即图中红线围成的区域内的所有元素都会加上一个常数$c$,但是我们仅仅需要让图中灰色区域加上一个常数$c$,因此,我们可以看到,将图中黄色线围成的区域和深蓝色线围成的区域减去后,图中粉色区域被多减了一次,因此要加上图中粉色区域,此时的结果就为灰色矩阵内所有元素都加上了常数$c$。根据差分定义,我们可以容易见得,黄色线围成的区域受到差分矩阵内$(x1,y2+1)$影响,深蓝色区域受到$(x2+1,y1)$的影响,粉色区域受到$(x2+1,y2+1)$影响,因此即可得到上述操作
二维差分模板
```c++
#include <iostream>
using namespace std;
const int N = 1010;
int n, m, q;
int a[N][N], b[N][N];
void insert(int x1, int y1, int x2, int y2, int c)

b[x1][y1] += c;
b[x2 + 1][y1] -= c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y2 + 1] += c;

int main()

cin >> n >> m >> q;
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 ++ )
insert(i, j, i, j, a[i][j]);
while (q -- )

int x1, y1, x2, y2, c;
cin >> 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 ++ )
b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];
for (int i = 1; i <= n; i ++ )

for (int j = 1; j <= m; j ++ )
cout << b[i][j] << " ";
cout << endl;

return 0;

### 三维差分
三维差分的模板代码比较少见。三维差分比较复杂,并且一般笔试或者竞赛中不会出现,因此我们这里仅仅作为一个了解内容,可以不必在意。如果遇到,可以根据二维差分来做推广。
元素的值用三维数组 $a[][][]$ 来定义,差分数组 $b[][][]$ 也是三维的。把三维差分想象成在立体空间上的操作。一维的区间是一个线段,二维是矩形,那么三维就是立体块。一个小立体块有 $8$ 个顶点,所以三维的区间修改,需要修改 $8$ 个$b[][][]$​ 值。可以类比下图来看(图片来自csdn)
![0QDKRYPKQ`9GON26NX56.png](https://s2.51cto.com/images/20220228/1646013154600129.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)
类比二维差分,三维差分要解决的操作就是给一个立方体内的所有元素都加上或者减去一个常数$c$
我们直接给出操作
```c++
b[x1][y1][z1]       += c;   //前面:左下顶点,即区间的起始点
b[x2+1][y1][z1]     -= c;   //前面:右下顶点的右边一个点
b[x1][y1][z2+1]     -= c;   //前面:左上顶点的上面一个点
b[x2+1][y1][z2+1]   += c;   //前面:右上顶点的斜右上方一个点
b[x1][y2+1][z1]     -= c;   //后面:左下顶点的后面一个点
b[x2+1][y2+1][z1]   += c;   //后面:右下顶点的斜右后方一个点
b[x1][y2+1][z2+1]   += c;   //后面:左上顶点的斜后上方一个点
b[x2+1][y2+1][z2+1] -= c;   //后面:右上顶点的斜右上后方一个点,即区间终点的后一个点

笔记参考

Acwing算法基础课

以上是关于前缀和与差分的主要内容,如果未能解决你的问题,请参考以下文章

前缀和与差分

基础算法 --- 前缀和与差分

[知识点] 2.7 前缀和与差分

[知识点] 2.7 前缀和与差分

前缀和与差分

前缀和与差分