前缀和
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; }
5.前缀和应用场景及模版
-
应用场景:
-
某个静态数组某个**区间所有元素求和**
-
统计区间和是k的倍数的区间总共有多少个
-
-
模版:
一维前缀和:
-
int a[N], s[N]; //构造一维前缀和 for(int i = 1; i <= n; i++) { s[i] = s[i-1] + a[i]; } //求[i, j]区间的元素总和 int sum = s[j] - s[i-1];
二维前缀和:
-
int a[N]{N], s[N][N]; //构造二维前缀和 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]; } } //求(x1,y1)到(x2,y2)区间的元素总和 int sum = s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1];
-
6.例题
-
分析:
- 根据题目意思就是要求返回一维数组的某个区间和。
- 这道题就是经典的一维前缀和问题,已知 an 数组中的每一项,我们便可以通过数列的前
n
项和公式 Sn = Sn-1 + an 构造前缀和数组 Sn 。构造好前缀和数组后,若要求 [l, r] 区间内的元素和,直接 Sr - Sl-1 即可。
-
设计思路:
- 定义一个数组
a[N]
用于存储静态数组的数据,一边输入数据的同时通过S[i] = S[i-1] +a[i]
构造数组a[N]
的前缀和数组。 - 通过输入的
lr
,利用公式S[r] - S[l-1]
计算出区间元素和。
- 定义一个数组
-
代码:
-
#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++) { scanf("%d", &a[i]); s[i] = s[i-1] + a[i] } while(m -- ) { int l, r; scanf("%d%d", &l, &r); printf("%d\n", s[r] - s[l-1]); } return 0; }
-
-
分析:
- 题目意思就是要求返回二维数组的某个区间和。
- 这是一个典型的二维前缀和问题。通过输入的二维数组
a[N][N]
,利用二维前缀和递推式,构造出二维前缀和数组s[N][N]
。 - 求
[x1, y1]到[x2, y2]
区间所有元素和:s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]
。
-
设计思路:
- 定义一个数组
a[N][N]
用于存储静态数组的数据,一边输入数据的同时通过递推式s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j]
构造数组a[i][j]
的前缀和数组。 - 通过输入的
x1,y1,x2,y2
,利用公式s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]
计算出区间元素和。
- 定义一个数组
-
代码:
-
#include <iostream> using namespace std; const int N = 1010; int n, m, q; int a[N][N], 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", &a[i][j]); s[i][j] = s[i][j-1] + s[i-1][j] - s[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", s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]); } return 0; }
-
-
分析:
-
根据题意,要求返回激光炸弹能炸掉地图上的最大总价值。
-
每个激光炸弹可以摧毁包含在
RxR
区域内的目标价值(不含边界上的点)。换而言之,题目要求的是所有RxR
区间的目标和最大值,一个区间的目标和有不同的取值,如下图所示: -
由上图可知,一个区间的目标和最大时,应该是上图2所示的炸法,即取
RxR
个点位。求区间和,就可以用典型的**二维前缀和**。 -
对于每个
RxR
个点位总和进行遍历,统计出最大值即可。 -
时间复杂度分析:地图总的目标点位个数为
5001x5001
,构造前缀和数组所用的最大时间为5001x5001
,遍历RxR
区间的最大次数为5001
,因此所花的总时间为5001x5001+5001 << 1e9
,不存在超时的问题。 -
空间复杂度分析:地图上总的点位数为
5001x5001
,我们定义原数组为 int 类型a[5001][5001]
,前缀和数组为 int 类型s[5001][5001]
,所申请的总的内存空间为5001x5001x4/(1024x1024) = 200MB
,很显然,存在爆内存的问题,因此我们只能开一数组s[N][N]
同时用作原数组和前缀和数组。 (1MB = 1024x1024Byte
)
-
-
设计思路:
- 先定义一个二维数组
s[N][N]
用于存储原数组和前缀和数组。定义两个变量 ,n
表示地图的宽度,m
表示地图的长度。 - 接着定义一个变量
r
表示激光炸弹的爆炸范围,并求r = min(r, 5001)
,用r
对n,m
进行初始化。 - 然后循环输入目标的坐标和价值,同时更新
n,m
的值。 - 利用二维前缀和递推式
s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j]
构造前缀和数组。 - 最后遍历每个
rxr
区间和,统计出价值最大的那个数值返回即可。
- 先定义一个二维数组
-
代码:
-
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 5010; int n, m;//n表示x坐标最大值,m表示y坐标最大值 int s[N][N];//该数组既是原数组,又是前缀和数组 int main() { int cnt, r; cin >> cnt >> r; r = min(5001, r); n = r; m = r; for(int i = 0; i < cnt; i++) { int x, y, w; scanf("%d%d%d", &x, &y, &w); x ++; y++; //横纵坐标都加一是为了,在预处理前缀和不用考虑边界问题,横纵坐标均大于等于1 s[x][y] += w; n = max(x, n); m = max(y, m); } //构造前缀和数组 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] + s[i][j]; } } int res = 0; for(int i = r; i <= n; i++) { for(int j = r; j <= m; j++) { res = max(res, s[i][j] - s[i-r][j] - s[i][j-r] + s[i-r][j-r]); } } cout << res <<endl; return 0; }
-
-
分析:
- 根据题意,从数列的第一位查询区间
[i,j]
的元素和是否是k
的倍数,统计此类区间的个数。 - 首先我们从最暴力的方法做起,先从
0到n
枚举i
,接着从i到n
枚举j
,然后再从i~j
统计总和,最后特判一下是否是k的倍数。总的时间复杂度是O(n^3)
,根据题目要求显然n
取1e6
的时候肯定超时。 - 对于区间和问题,我们可以采用前缀和的方法来空间换时间,在计算
i~j
的总和上,我们采用前缀和求解,总的时间复杂度为O(n^2)
,但显然这样还是不行,还是会出现超时的情况。 - 我们对题目的条件在前缀和思路下,再细细分析一下,看能不能再优化一层循环。已知前缀和数组
s[N]
,区间和sum = s[j] - s[i-1]
,要判断区间是否是k
倍区间,只需判断是否满足s[j] - s[i-1] % k == 0
,该判断式子也可以变换成s[j] % k == s[i-1] % k
。 - 对于以上式子我们不难发现一个思路:由于
i-1
一定比j
小,所以在遍历s[j] % k
的大小时,s[i-1] % k
的大小已经遍历过了。因此我们只需要在遍历s[j]
的同时,用数组cnt[s[j]%k]++
记录在j
之前余数等于s[j]%k
的s[i]
的个数,每一次遍历s[j]
,res += cnt[s[j]%k]
,即可统计出所有的k
倍区间的个数。
- 根据题意,从数列的第一位查询区间
-
设计思路:
- 首先定义一个long long 类型的数组
s[N]
用于存储前缀和,另外定义一个数组cnt[N]
用于存储余数相同的s[i]
的个数。 - 通过输入的元素构造前缀和数组,并且定义一个变量
res
用于存储最终统计的k
倍区间个数。 - 接着遍历
s[j]
更新res
的同时,更新cnt[s[j]]
的大小。 - 最后输出
res + cnt[0]
。(原因是每次更新res
都是res += cnt[s[j]%k]
,res
加的cnt[s[j]%k]
表示在j
之前有多少个s[i]
对k
求余等于s[j]%k
,不包括当前s[j]
。对于s[j]%k == 0
,res还要加上s[j]
本身,因此最终答案是res + cnt[0]
。
- 首先定义一个long long 类型的数组
-
代码:
-
#include <iostream> using namespace std; const int N = 100010; long long s[N], cnt[N]; int n, k; int main() { scanf("%d %d", &n, &k); for(int i = 0; i <= n; i ++) { scanf("%lld", &s[i]); s[i] += s[i-1]; } long long res = 0; for(int i = 0; i <= n; i++) { ans += cnt[s[i]%k]; cnt[s[i]%k]++; } printf("%lld", (long long)res+cnt[0]); return 0; }
-