(一)前缀和的定义
假设有一个字符串ABCDE,那么A,AB,ABC,ABCD,ABCDE就是这个单词的前缀,就是从第一个字符开始,依次往后拼接。而E,ED,EDC,EDCB,EDCBA称为这个单词的后缀。
那么对于一个int类型数组的前缀,例如数组a=[0,12,62,33,4,55],我们维护一个由前缀的和组成的一个数组sum,sum[i]表示数组中a[0]~a[i]的和。其中a[0]默认为0,若:
- sum[0]=a[0]=0
- sum[1]=a[0]+a[1]
- sum[2]=a[0]+a[1]+a[2]
- sum[3]=a[0]+a[1]+a[2]+a[3]
- sum[4]=a[0]+a[1]+a[2]+a[3]+a[4]
那么sum就是数组a的前缀和数组。
(二)前缀和的作用
前缀和我们可以用预处理(边输入边维护)来进行存储,能大大降低查询的世界复杂度。因为前缀和的主要目的是求子数组的和的大小。例如,元素a[1]到a[3]的和:a[1]+a[2]+a[3],这个for循环进行的话需要用O(n)的时间,而用前缀和sum[3]-sum[0]来解决就是O(1)的复杂度,为什么sum[3]-sum[0]就是a[1]到a[3]的和呢?
我来解释一下,给一个更鲜明的例子,求元素a[3]到a[5]的和:a[3]+a[4]+a[5],前缀和用起来就是sum[5]-sum[2],为什么呢?我们来拆开看看:
sum[5]=a[0]+a[1]+a[2]+a[3]+a[4]+a[5];
sum[2]=a[0]+a[1]+a[2];
看到什么没有,前面的三个a[0]+a[1]+a[2]可以抵消掉,就成为了a[3]+a[4]+a[5],这样利用前缀和的思想,时间复杂度优化到了最优。
(三)前缀和的应用
我们通过一道题来说明一下前缀和的应用。
给定一个长度为n的整数数组和一个整数k,你需要找到该数组中和为k的连续子数组的个数,
测试样例:
输入:
5 3
1 1 2 1 1
输出:
2
思路1:
利用for循环暴力枚举子数组,并且求和+计数,时间复杂度为O(n^3)。(如果数据大于了100,这个思路绝对Time Limit Exceeded(时间超时),所以要进行优化)!
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,k; //定义
scanf("%d%d",&n,&k); //输入
int a[n],sum=0; //定义数组和计数器
for(int i=0;i<n;i++) //for循环输入
scanf("%d",a[i]); //输入下标为i的数
for(int i=0;i<n;i++){ //区间i枚举
for(int j=0;j<n;j++){ //区间j枚举
int ans=0; //区间和计数数组
for(int x=i;x<=j;x++) //求出区间[i,j]的和
ans+=a[i]; //计数
if(ans==k) //比较
sum++; //计数
}
}
cout<<sum<<endl; //输出
return 0;
}
思路2:
我们还可以使用前缀和来解决这个问题,首先预处理出a的前缀数组sum,每次求出子数组的和,然后进行双重循环的区间枚举,然后与k进行比较,时间复杂度为O(n^2).(如果数据不大于1500,是可以AC的,还有优化空间)。
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,k; //定义
scanf("%d%d",&n,&k); //输入
int sum[n+1]={0},ans=0; //定义前缀和数组sum和计数器ans
sum[0]=0; //将下标为0的地址初始化为0
for(int i=1;i<=n;i++){ //进行循环n次读入
int a; //定义
scanf("%d",&a); //输入
sum[i]=sum[i-1]+a; //求前缀和
}
for(int i=1;i<=n;i++) //区间枚举i
for(int j=0;j<n;j++) //区间枚举j
if(sum[i]-sum[j]==k) //求出区间[i,j]的和与k比较
ans++; //计数器加1
cout<<ans<<endl; //输出计数器
return 0; //结束
}
思路3:
我们可以使用map(STL库定义的类),加上前缀和进行优化。在单用前缀和的思路中,我们要求一个结尾下标为i的子数组的和是否为k,就需要对j从0开始遍历到i-1,来找到是否存在sum[i]-k=sum[j]。那么我们只用map来存前i个元素的前缀和,把出现的次数(>i)之前的前缀和的值保存下来,最后判断map中是否包含sum[i]-k即可。
时间复杂度为O(n),已经算是很快的了,数据大到几百万都可以AC了。
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,k; //定义
scanf("%d%d",&n,&k); //输入
int sum[n+1]={0},ans=0; //定义前缀和数组sum和计数器ans
sum[0]=0; //将下标为0的地址初始化为0
map<int,int> p; //定义map
for(int i=1;i<=n;i++){ //进行循环n次读入
int a; //定义
scanf("%d",&a); //输入
sum[i]=sum[i-1]+a; //求前缀和
p[sum[i]]++; //进行存储
}
for(int i=0;i<n;i++) //进行区间判断
ans+=p[sum[i]+k]; //计数
cout<<ans<<endl; //输出
return 0; //结束
}
总结:
前缀和是一个非常有用且能大幅度优化时间复杂度的算法,希望大家多多学习,以后几篇文章我会多讲一些前缀和的习题。