声明
前缀和 与 差分可以理解为互逆的 , 求前缀和 跟 差分通常设数组首项下标为1,方便思考的计算。
一维前缀和介绍
对于一维数组 a[n] , 存在数组 sum[n],使得 sum[i] = a[1] + a[2] + … + a[i]
前缀和优势
以O(1)的时间复杂度得到某块区间的总和
举个例子:
输入一个长度为 n 的整数序列。
接下来再输入 m 个询问,每个询问输入一对 l,r。
对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。
常规算法 是两遍for循环求区间和,题目范围小还可以接受,若是开到1e5+,必然会爆int的。所以这里使用前缀和让时间复杂度降到O(1)
一维前缀和相对较简单,不做过多介绍
二维前缀和介绍
如果数组变成二维数组又该怎么做呢,做法与一维前缀和相类似,只是多了个容斥原理
如果我们要求给定坐标的前缀和,如图
若果我们要求(3,3)的前缀和 , 先算出(3,2),(2,3)的和,再减去图中红绿交汇的部分即(2,2) ,再加上坐标(3,3)的数值,就可以求出(3,3)的和
因此,二维前缀和公式:s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] +a[i][j]
那么怎么求任意矩阵任意位置坐标的和呢
比如
我们要求(x1,y1)到(x2,y2)前缀和,那么就是求图中红色区域,那么用(x2,y2)的前缀和(3*3矩阵)- 绿色区域 - 蓝色区间 + 绿兰交汇区域
因此,求任意区域的前缀和公式也很容易得出:
ans = s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]
二维前缀和模板题
很典型的二维前缀模板题
思路 : 先求前缀和 , 再利用公式ans = s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1] 求出答案即可
代码如下
# include <iostream>
using namespace std ;
const int N = 1e3 + 10 ;
int a[N][N];
int sum[N][N];
int main(){
int m , n , q;
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 ++)
sum[i][j] = sum[i-1][j] + sum[i][j-1] -sum[i-1][j-1] +a[i][j];
// 求指定区间的和
while (q --){
int x1 , x2 , y1 , y2 ;
cin >> x1 >> y1 >> x2 >> y2 ;
cout << sum[x2][y2] - sum[x1-1][y2] - sum[x2][y1-1] + sum[x1-1][y1-1] <<endl;
}
return 0 ;
}
一维差分
类似于数学中的求导和积分,差分可以看成前缀和的逆运算。
首先我们给定一个原数组a:a[1], a[2], a[3], a[n];
然后我们构造一个数组b : b[1] ,b[2] , b[3], b[i];
使得 a[i] = b[1] + b[2 ]+ b[3] +, + b[n];
也就是说,我们原始的数组是我们构造的数组的前缀和。
那么我们如何求差分数组呢?
b[1] = a[1] - a[0];
b[2] = a[2] - a[1];
b[3] =a [3] - a[2];
…
b[n] = a[n] - a[n-1];
因此通过数组b,我们可以在O(1)内对原始数组进行改造
一维差分例题
题意很简单,就是给定区间 , 对原数组进行修改。
常规做法,两层循环,复杂度O(n*m) 写法很简单,但必爆int。
简单梳理一下,为什么会跟差分有关联。
给定区间[l ,r ],让我们把a数组中的[ l, r]区间中的每一个数都加上c,即 a[l] + c , a[l+1] + c , a[l+2] + c , a[r] + c。
所以,只要对差分数组b[n]加上c,那么它的前缀和数组a[n]也会因此而改变。比如b[k]+c,那么a[k]及其往后的数全部要加k。
所以当给定我们一个区间,做法也是同样的:
首先让差分数组b中的 b[l] + c ,通过前缀和运算,a数组变成 a[l] + c ,a[l+1] + c, a[n] + c。
然后 b[r+1] - c, 通过前缀和运算,a数组变成 a[r+1] - c,a[r+2] - c,a[n] - c。
这里为什么要有b[r+1] - c 呢?下面画图解释一下:
我们要让[l,r]区间的原始数组a的数值变化,通过b[l]+c,让a[l]及其之后的数组都+c,但r之后的数我们并不想让它+c,所以我们还要让r之后的数组-c。
所以,b[r+1] - c就是这个道理。
代码块:
# include <iostream>
using namespace std ;
const int N = 1e5 + 10 ;
int a[N];
int b[N];
int main(){
int n , t ;
cin >> n >> t;
for (int i = 1 ; i <= n ; i ++){
cin >> a[i];
b[i] = a[i] - a[i-1];
}
while (t --){
int l , r , c ;
cin >> l >> r >> c ;
b[l] += c;
b[r+1] -= c;
}
for (int i = 1 ; i <= n ; i++){
b[i] += b[i-1];// 求前缀和,就是求a的各项
cout << b[i] << " ";
}
return 0 ;
}
二维差分
二维差分和二维前缀和都运用到了容斥原理
同样,a[][]数组是b[][]数组的前缀和数组,那么b[][]是a[][]的差分数组。
那么a数组中a[i][j]是b数组左上角(1,1)到右下角(i,j)所包围矩形元素的和。
如何构造二维差分数组
对于差分,我们始终要记住,b[i][j]的改变会影响a[i][j]及其之后的元素。
若b数组已经构造好,让指定区域的元素加上c
b[x1][y1] + = c;
b[x1][y2+1] - = c;
b[x2+1][y1] - = c;
b[x2+1][y2+1] + = c;
我们画个图来理解一下
比如我们要改变的区间是在(x1,y1)到(x2,y2)
首先对b[x1][y1]+c ,就是图中红色区域。
再对绿色和紫色区域操作: b[x1][y2+1] - c ,b[x2+1][y1] - c.
又因为粉色重叠部分多减了一次,再加上 b[x2+1][y2+1] + c。
再详细分析一下
我们要求这个区域:
必然先要求这个区域:
再求这两个区域:
最后减去交汇的区域
我们对原始数组进行区域增减,实际上就是对b数组的增减,通过这个方法创造差分数组。
方法:
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
b[i][j] += c;
b[i+1][j] -= c;
b[i][j+1] -= c;
b[i+1][j+1] += c;
}
就等同于对原始数组进行修改
for (int i = 1 ; i <= n ; i ++)
for (int j = 1; j <= m ; j ++)
a[i][j] += c;
假设a,b数组均为空,实际上不为空。我们每次让以(i,j)为左上角到以(i,j)为右上角面积内元素(其实就是一个小方格的面积)去插入 c=a[i][j],等价于原数组a中(i,j) 到(i,j)范围内 加上了 a[i][j] ,因此执行n*m次插入操作,就成功构建了差分b数组.
二维差分习题
和上面讲的思路完全一样,直接上代码
#include <iostream>
using namespace std;
const int N = 1000 + 10;
int a[N][N];
int b[N][N];
int n, m, q;
void insert(int x1, int y1, int x2, int y2, int c){ // 写成函数形式方便操作
b[x1][y1] += c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y1] -= 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++){
scanf("%d",a[i] + 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++){
a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + b[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;
}
注意a是b的前缀和, b[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1],移项就能求出a[i][j]。