原文链接:前缀和与差分 图文并茂 超详细整理(全网最通俗易懂)_林小鹿@的博客-CSDN博客_前缀和差分
1.前缀和的定义
-
前缀和是指某序列的前n项和,可以把它理解为数学上的数列的前n项和,而差分可以看成前缀和的逆运算。合理的使用前缀和与差分,可以将某些复杂的问题简单化。
(数学角度上分析就是说,给定一数列an,其前n项和为Sn,那么Sn构成的数列是an数列的前缀和数列;而反过来,an数列被称为是Sn数列的差分数列。这种关系就像是导数和不定积分一样,求导后的函数是原函数的导数;原函数是求导后函数的不定积分。)
2.怎么利用前缀和
-
给出一个问题:
输入一个长度为n的整数序列。接下来再输入m个询问,每个询问输入一对l, r。对于每个询问,输出原序列中从第l个数到第r个数的和。
-
法一:暴力解法
先输入长度为n的数组,对数组进行遍历,每一次遍历找出对应的位置元素进行相加,遍历m次
代码如下:
#include <iostream> using namespace std; int main() { int n,m;cin>>n>>m; int a[100010]; for(int i =1;i<=n;i++) { cin<<a[i]; } while(m--) { int l,r,sum=0;cin>>l>>r; for(int i=l;i<=r;i++) { sum+=a[i]; } cout<<sum; } }
(这样的时间复杂度为O(n * m),如果n和m的数据量稍微大一点就有可能超时,而我们如果使用前缀和的方法来做的话就能够将时间复杂度降到O(n + m),大大提高了运算效率。)
-
法二:前缀和法
先创建一个数组S[n]用于存放前缀和,接着在输入数据an时对数据进行处理,即求出对应的Sn,放进数组S[n]中;紧接着就是输入l和r,每输入一对l,r就求出S[r+1]-S[l]的值,所得值就是所求的区间值
代码如下:
#include <iostream> using namespace std; int main() { int S[100010]; int n,m;cin>>n>>m; for(int i =1;i<=n;i++) { int a;cin>>a; S[i]=S[i-1]+a; } while(m--) { cout<<S[r]-S[l-1]<<endl; } return 0; }
(很明显啊,该方法的空间复杂度较法一小,空间复杂度为O(n+m),大大提高了运算效率)
3.一维前缀和
-
一维前缀和,顾名思义就是一维数组的前缀和,即一维数组中前n个元素的和。
-
规律:
-
一维数组an的某一前缀和:Sn=Sn-1+an
-
一维数组an某一区间[l, r]的和:=S[r]-S[l-1]
-
-
总结:
4.二维前缀和
-
定义:
给定一个二维数组a[] [],s[i] [j]表示二维数组中,左上角(1, 1)到右下角(i, j)所包围的矩阵元素的和(s[i] [j]所代表的值并不是从[1] [1]到[i] [j]的所有元素之和。)
-
求二维数组某区间的前缀和;
以
(x1, y1)
为左上角,(x2, y2)
为右下角的子矩阵的和为:s[x2, y2] - s[x1 - 1, y2] - s[x2, y1 - 1] + s[x1 - 1, y1 - 1]
-
规律:
-
二维数组某一前缀和:s[i, j]=s[i, j-1]+s[i-1, j]-s[i-1, j-1]+a[i,j]
-
二维数组某一区间和:=s[x2, y2] - s[x1 - 1, y2] - s[x2, y1 - 1] + s[x1 - 1, y1 - 1]`
-
-
例题:
输入一个n行m列的整数矩阵,再输入q个询问,每个询问包含四个整数x1, y1, x2, y2,表示一个子矩阵的左上角坐标和右下角坐标。
对于每个询问输出子矩阵中所有数的和。
输入格式 第一行包含三个整数n,m,q。
接下来n行,每行包含m个整数,表示整数矩阵。
接下来q行,每行包含四个整数x1, y1, x2, y2,表示一组询问。
输出格式
共q行,每行输出一个询问的结果。
思路:先创建一个数组用于储存二维数组an的数据;接着将数据输入到数组s[N] [N];再利用二维数组前缀和的公式s[i, j]=s[i, j-1]+s[i-1, j]-s[i-1, j-1]+a[i,j]对数组s进行遍历(对an数组进行遍历并将Sn的数据覆盖,是为了方便加上a[i, j],将原来数组s[N] [N]的数据覆盖,获得Sn数组。
代码如下:
#include <iostream> using namespace std; const int N = 1010; int n, m, q; int s[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", &s[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] - s[i - 1][j - 1]; while (q -- ) { int x1, y1, x2, y2; scanf("%d%d%d%d", &x1, &y1, &x2, &y2); printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]); } return 0; }
-
总结:
-
前缀和的运用一般用于区间求和
-
前缀和的运用:先构造一个前缀和数列即Sn,再找出Sn和an区间和的关系,利用关系求区间和。
-
5.差分的定义
-
差分可以看成前缀和的逆运算,即可以理解为给定数列Sn,求an数列,所求的an数列被称为Sn数列的差分数组。
6.差分有何用
-
可用于给一个给定数组某一区间的所有元素加上一个常数C
-
给出一个问题:
给定区间
[l, r ]
,让我们把a
数组中的[l, r]
区间中的每一个数都加上c
,即a[l] + c , a[l + 1] + c , a[l + 2] + c ,,,,,, a[r] + c
;
-
法一:暴力解法
暴力做法是
for
循环l
到r
区间,时间复杂度O(n)
,如果我们需要对原数组执行m
次这样的操作,时间复杂度就会变成O(n * m)
。有没有更高效的做法吗? 考虑差分做法,(差分数组派上用场了)。
-
法二:差分解法
将a[n]看做是辅助数列b[n]的前缀和数列,即a[i]=b[1]+b[2]+,,,+b[i];
所以对于将a[n]中区间[l,r]上每个数都加上常数c,我们先给b[l]+c,此时对应的a数组的区间[l,r]就变成了a[l]+c,a[l+1]+c,...,a[r]+c,a[r+1]+c、、、,
为了避免从r以后的数列a的元素都加c,我们对b[r+1]-c,则a数组的区间[l,r]就变成了a[l]+c,a[l+1]+c,...,a[r]+c,a[r+1]+c-c,a[r+2]+c-c、、、,即a[l]+c,a[l+1]+c,...,a[r]+c,a[r+1],a[r+2],、、、
因此,我们只要构造出b数组就可以对b数组中对应的元素进行加C从而影响a数组对应的值从而达到我们想要的目的。
7.一维差分
-
定义:一维数组前缀和的逆运算
-
构建一维差分数组:(已知数组s,求其差分数组a)
-
a[i] = s[i] - s[i-1]
-
-
例题:
题目练习: AcWing 797. 差分
输入一个长度为n的整数序列。 接下来输入m个操作,每个操作包含三个整数l, r, c,表示将序列中[l, r]之间的每个数加上c。 请你输出进行完所有操作后的序列。
输入格式 第一行包含两个整数n和m。 第二行包含n个整数,表示整数序列。 接下来m行,每行包含三个整数l,r,c,表示一个操作。
输出格式 共一行,包含n个整数,表示最终序列。
思路:先构建两个数组a,b(a为前缀和数组,b为差分数组),接着在输入a数组数据时,利用公式b[i] = a[i] - a[i - 1]进行差分数组的构建,然后对构建好的差分数组b进行数据处理,即在对应的数据b[l]+c,b[r+1]-c从而达到影响数组a的目的,最后对数组b进行前缀和运算。
代码如下:
#include<iostream> using namespace std; const int N = 1e5 + 10; int a[N],b[N]; int main() { int n,m; scanf("%d%d", &n, &m); for(int i = 1;i <= n; i++) { scanf("%d", &a[i]); b[i] = a[i] - a[i - 1]; //构建差分数组 } int l, r, c; while(m--) { scanf("%d%d%d", &l, &r, &c); b[l] += c; //表示将序列中[l, r]之间的每个数加上c b[r + 1] -= c; } for(int i = 1;i <= n; i++) { b[i] += b[i - 1]; //求前缀和运算 printf("%d ",b[i]); } return 0; }
8.二维差分
-
定义:二维差分实际就是二维前缀和的逆运算
-
构造二维差分数组:
由于二维不像一维那样好理解,所以我们先从如何对差分数组中元素处理,来影响二维数组a的值,从而使数组a某特定区间的元素都加C
-
数据处理:
已知原数组
a
中被选中的子矩阵为 以(x1,y1)
为左上角,以(x2,y2)
为右下角所围成的矩形区域;即对该区间内a数组的每一元素都加c;假定我们已经构造好了
b
数组,类比一维差分,我们执行以下操作 来使被选中的子矩阵中的每个元素的值加上c
b[x1][y1] + = c
;b[x1,][y2+1] - = c
;b[x2+1][y1] - = c
;b[x2+1][y2+1] + = c
;至于为什么要对差分数组b的数据进行这样的处理,就可以使得a数组对应的区间内的元素加c,课参考林大佬的图解 ,原文链接:前缀和与差分 图文并茂 超详细整理(全网最通俗易懂)_林小鹿@的博客-CSDN博客_前缀和差分
接着我们可以对以上对数组b进行数据处理的操作封装为一个函数
void insert(int x1,int y1,int x2,int y2,int c) { //对b数组执行插入操作,等价于对a数组中的(x1,y1)到(x2,y2)之间的元素都加上了c b[x1][y1] += c; b[x2 + 1][y1] -= c; b[x1][y2 + 1] -= c; b[x2 + 1][y2 + 1] += c; }
该函数的意思就是对b数组进行插入数据操作,从而影响数组a,对a数组中的(x1,y1)到(x2,y2)之间的元素都加上了c;就是说数组b插入操作,插入数为c,数组a某区间元素将都加c
从以上角度出发,我们可以推出另一个结论,当我们需要给a数组某一个元素加C时同样可以对差分数组b插入操作。
因此,我们可以假设数组a原来是空数组,此时对应的差分数组b也是空数组,对a数组逐个元素添加上a[i] [j]使得a数组变回原来题目给定的数组。
在这个对a空数组每个空元素加a[i] [j]的过程,实际上就是对对应的b数组的数据进行插入操作,插入数c为a[i] [j]。因此,在a数组变回原来数组之际,其对应的差分数组b也跟着构建完成。
通过以上思路,我们明白了如何构建数组a的差分数组b,就是利用封装函数对差分数组b进行插入操作,插入数为a[i] [j]
-
例题:
题目练习: AcWing 798. 差分矩阵 输入一个n行m列的整数矩阵,再输入q个操作,每个操作包含五个整数x1, y1, x2, y2, c,其中(x1, y1)和(x2, y2)表示一个子矩阵的左上角坐标和右下角坐标。 每个操作都要将选中的子矩阵中的每个元素的值加上c。 请你将进行完所有操作后的矩阵输出。
输入格式 第一行包含整数n, m, q。 接下来n行,每行包含m个整数,表示整数矩阵。 接下来q行,每行包含5个整数x1, y1, x2, y2, c,表示一个操作。
输出格式 共 n 行,每行 m 个整数,表示所有操作进行完毕后的最终矩阵。
代码如下:
#include<iostream> #include<cstdio> using namespace std; const int N = 1e3 + 10; 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() { int n, m, 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++) { 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++) { printf("%d ", b[i][j]); } printf("\n"); } return 0; }