有一个半月没有更新了,可能是因为前段时间琐碎的事情比较多,本文介绍一下前缀和,前缀和是处理数组的一种技巧,用一个数组来存储下标从1到i的和,通常情况下这样可以减少时间复杂度,下面通过列举几道题来感受一下这个方法,下面的题目大多来自于Acwing。

Acwing3723 字符串查询

给你单词S和Q个询问,每次询问时,你会得到整数A、B、C和D。我们令单词X是由S的第A到B个字母组成,单词Y是由S的第C到D个字母组成。你需要回答,是否能够重新排列单词Y中的字母,得到单词X。

输入格式:第一行输入一个单词S,仅由小写字母组成。第二行输入一个正整数Q。接下来的Q行,每行四个整数A,B,C,D。

输出格式:每次询问,如果能,输出DA,否则输出NE。 1<=|S|<=50000。

题目分析:

简单来说,就是看这两个区间内的(a,z)的字符个数是否相同,一个最朴实无华的方法就是把这两个字字符串截下来,然后对其进行排序,最后再挨个比较,当然,不出意外的话会超时。这种情况下的时间复杂度是O((b-a)*log(b-a)),最差情况就是 O(nlog(n)),比这个复杂度小的只有O(n)(明显这道题是不可能有O(1)的),所以就需要寻找一种方法,在遍历一遍或几遍S后,便可判断出结果。

我们可以预处理一个前缀和数组 sum[i][c],表示前 i 个字符中,字母 c 出现的次数。这样对于任意区间 [l, r],字母 c 的出现次数就是 sum[r][c] - sum[l-1][c]。只要两个区间内26个字母的出现次数都相等,就说明可以通过重排得到相同的字符串。

#include<iostream>
#include<algorithm>
#include<string>
using namespace std;
const int N=50010,M=27;
int s[N][M];
string str;

bool check(int a,int b,int c,int d){
	for(int i=0;i<26;i++){
		if(s[b][i]-s[a-1][i]!=s[d][i]-s[c-1][i]){
			return false;
		}
	}
	return true;
}

int main(){
	cin>>str;
	for(int i=1;i<=str.size();i++){
		s[i][str[i-1]-'a']=1;
	}
	for(int i=0;i<26;i++){
		for(int j=1;j<=str.size();j++){
			s[j][i]+=s[j-1][i];
		}
	}
	int q;
	cin>>q;
	while(q--){
		int a,b,c,d;
		cin>>a>>b>>c>>d;
		if(check(a,b,c,d)){
			cout<<"DA"<<endl;
		}
		else{
			cout<<"NE"<<endl;
		}
	}
	return 0;
}

先把每个位置上出现的字母标记为 1。然后对每个字母做前缀和,s[j][i]就是前j个字符中第i个字母出现的总次数。最后对两个区间内出现的每个字母个数进行比较。这种方法通过预处理数组得到每一个字符前缀和,处理的时间复杂度是O(n),可以方便后续的比较。

Acwing1230 K倍区间

给定一个长度为N的数列,$A_1,A_2,… A_N$,如果其中一段连续子序列$A_i,A_{i+1},…A_{j}$之和是K的倍数,我们就称区间[i,j]是K倍区间。请求出数列中一共有多少个K倍区间。

输入格式:第一行包含两个整数N和K。以下N行每行包含一个整数$A_{i}$。
输出格式:输出一个整数,代表K倍区间的数目。

题目分析

一个最简单的想法就是通过前缀和得到一个新的数组nums,然后用双循环来遍历这个新的数组,判断每个区间是否是K的倍数,这个复杂度是$O(n^2)$,绝对也会超时,这道题明显是希望我们可以找到一个方法,通过遍历一次数组就可以得到结果,我们可以对nums[i]对K取余,假如说nums[i]与nums[j]的余数一致,那说明[i,j]就是K倍区间。当我们遍历到nums[m]余数为a,只需要看前面有几个余数为a的即可。取余运算在这种算法题目中是非常常见的,一般可以多往这方面想想。

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

int main(){
    int N,K;
    cin>>N>>K;
    vector<int>nums(N+1);
    vector<int>sum(N+1);
    long long ans=0;
    vector<int>v(K);
    for(int i=1;i<=N;i++){
        cin>>nums[i];
        sum[i]=(sum[i-1]+nums[i])%K;
        ans+=v[sum[i]];
        v[sum[i]]++;
    }
    cout<<ans+v[0]<<endl;
    return 0;
}

还需要注意的一小点就是余数为0的,本身就可以作为一个区间,所以最后需要再加上v[0]。

Acwing 3574乘积数量

给定一个长度为n且不包含0的整数序列$a_1,a_2,…,a_n$。请你计算以下两值:
1.使得$a_{l}×a_{l+1}×…×a_{r}$为负的索引对(l,r)(l<=r)的数量。
2.使得$a_{l}×a_{l+1}×…×a_{r}$为正的索引对(l,r)(l<=r)的数量。

输入格式:
第一行输入一个整数n。
第二行包含n个整数$a_1,…,a_n$。

输出格式:
输出单个空格隔开的两个整数,分别表示负的索引对数和正的索引对数。

数据范围:

$1 \leq n \leq 2×10^5$

$-10^9 \leq a_i \leq 10^9$

题目分析

在这到题里面,$a_i$只需要考虑是不是正的就行了,为了方便一些,我打算把大于0的全部都换成1,小于0的全部都换成-1,这道题里就不做前缀和了,直接前缀积,反正现在的数全部都是1和-1,相乘之后也只会是1和-1,假如nums[j]=-1,那我们可以看前面有几个nums[i]=-1,这样[i+1,j]就是正的索引对;相反,前面有几个nums[i]=1,这样[i+1,j]就是负的索引对;nums[j]=1也是类似的。

#include<iostream>
using namespace std;
const int N=2e5+1;
long long arr[N]={1};

int main(){
	int n;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>arr[i];
		if(arr[i]>0){
			arr[i]=1;
		}
		else{
			arr[i]=-1;
		}
	}
	int plus=0;
	int minus=0;
	long long retp=0;
	long long retm=0;
	for(int i=1;i<=n;i++){
		arr[i]=arr[i]*arr[i-1];
		if(arr[i]>0){
			retp+=plus;
			retm+=minus;
			plus++;
		}
		else{
			retp+=minus;
			retm+=plus;
			minus++;
		}
	}
	cout<<retm+minus<<" "<<retp+plus<<endl;
	return 0;
}

这道题和前面的那道K倍区间是类似的。

Acwing3493.最大的和

给定一个长度为n的正整数数列$a_1,a_2,…,a_n$。初始时,数列中的元素要么处于可选状态,要么处于不可选状态。

你可以选择一个长度恰好为k的区间[i,i+k-1],使得$a_i \sim a_{i+k-1}$这k个元素的状态全部变为可选状态。

请问,在经过次操作之后,所有处于可选状态的元素之和最大是多少。

输入格式:

第一行包含两个整数n和k。

第二行包含n个整数$a_i$.

第三行包含一个长度为n的01序列,如果第i个数为1,表示$a_i$的初始状态为可选,如果第i个数为0,表示$a_i$的初始状态不可选。

输出格式:

一行一个整数,表示答案。

题目分析

简单而言,这道题目的意思就是有一个数组,目前只有部分元素使可选的,现在可以使一个长度为k的区间内的全部都变为可选,使处于可选状态的元素之和最大,
原来的数组可选部分的元素和是固定的,现在一个区间内的数组全部变为可选,也就是说这样会有一个增加量,想要元素之和最大,那么这个增加量需要是最大的。

此时,我们可以用两次前缀和,第一次就是对a[i]原数组,第二次是对a[i]*b[i],然后遍历一次数组,找到最大的增加量即可。

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long ll;

const int N=1e5+1;
int n,k;
ll a[N],b[N];
ll aa[N]={0};
ll bb[N]={0};

int main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		aa[i]=aa[i-1]+a[i];
	}
	for(int i=1;i<=n;i++){
		cin>>b[i];
		bb[i]=bb[i-1]+a[i]*b[i];
	}
	ll cha=0;
	for(int j=k;j<=n;j++){
		cha=max(cha,(aa[j]-aa[j-k])-(bb[j]-bb[j-k]));
	}
	cout<<cha+bb[n]<<endl;
	return 0;
}

Acwing5295

给定一个长度为n的整数数组$a_0,a_1,…,a_{n-1}$.

函数sum(l,r)的定义如下:

  • 如果$l=r$,则$sum(l,r)=0$。
  • 如果$l<r$,则$sum(l,r)=\sum_{i=l}^{r-1}a_{i}$。

请你找到一个三元组(x,y,z),要求:

  1. $0 \leq x \leq y \leq z \leq n$
  2. sum(0,x)-sum(x,y)+sum(y,z)-sum(z,n)的值是尽可能大的。

输入格式:

第一行包含整数n。
第二行包含n个整数$a_0,a_1,…,a_{n-1}$。

输出格式:

在一行内输出三个整数,表示满足条件的整数三元组(x,y,z)。

数据范围:

$1 \leq n \leq 5000,-10^9 \leq a_i \leq 10^9$。

题目分析

首先肯定是需要对原来的数组做前缀和的,接下来需要确定三个数,最直接的方法应该是叠加三个for循环,这个时间复杂度是$O(n^3)$,但肯定不会通过全部的测试用例。我们可以先确定中间的y,然后在(1,y)中寻找x使得sum(0,x)-sum(x,y)是最大的,然后在(y,n)中间寻找z,使得sum(y,z)-sum(z,n)是最大的,这样在确定y的条件下,就可以得到需要的最大值。为了得到整体的最大值,我们可以遍历整个数组来确定y,这样的时间复杂度是$O(n^2)$。

#include<iostream>
#include<vector>
using namespace std;
int n;


long long getSum(int l,int r,vector<long long>&nums){
	return nums[r-1]-nums[l-1];
}

int main()
{
	cin>>n;
	vector<long long>nums(n+1);
	for(int i=1;i<=n;i++){
		cin>>nums[i];
	}
	for(int j=1;j<=n;j++)
	{
		nums[j]+=nums[j-1];
	}
	
	long long maxSum=-0x3f3f3f3f3f3f3f3f;
	int x=1,y=1,z=1;
	for(int yy=1;yy<=n+1;yy++){
		long long res1=-0x3f3f3f3f3f3f3f3f;
		long long res2=-0x3f3f3f3f3f3f3f3f;
		int tx,tz;
		for(int xx=1;xx<=yy;xx++){
			long long tt=getSum(1,xx,nums)-getSum(xx,yy,nums);
			if(tt>res1){
				res1=tt;
				tx=xx;
			}
		}
		for(int zz=yy;zz<=n+1;zz++){
			long long tt=getSum(yy,zz,nums)-getSum(zz,n+1,nums);
			if(tt>res2){
				res2=tt;
				tz=zz;
			}
		}
		
		if(res1+res2>maxSum){
			maxSum=res1+res2;
			x=tx;
			y=yy;
			z=tz;
		}
	}
	cout<<x-1<<" "<<y-1<<" "<<z-1;
	return 0;
} 

Acwing3956

给定一个长度为n的数组$a_1,a_2,…,a_n$。

现在,要将该数组从中间截断,得到三个非空子数组,要求三个子数组内各元素之和都相等。

请问,共有多少种不同的截断方法。

输入格式:

第一行包含整数n。

第二行包含n个整数$a_1,a_2,…,a_n$

输出格式:
输出一个整数,表示截断方法的数量。

数据范围:
$1 \leq n \leq 10^5,-10000 \leq a_i \leq 10000$。

题目分析:

得到 三个非空子数组的和都相等,说明数组的和需要是3的倍数才行。这道题大概率也是得找出遍历一遍前缀和数组就能得到结果的方法,那么每当我们遍历到nums[j]时,看其是否满足是nums[n]的三分之二,然后再看其前面有多少个nums[i]是nums[n]的三分之一倍,这样就要求在遍历数组的时候将是nums[n]的三分之一的nums[i]的个数存储起来。

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int n;

int main()
{
	cin>>n;
	vector<long long>nums(n+1);
	for(int i=1;i<=n;i++){
		cin>>nums[i];
		nums[i]+=nums[i-1];
	}
	if(nums[n]%3!=0 || n<3){
		cout<<0<<endl;
	}
	else{
		int point=0;
		long long ans=0;
		for(int i=1;i<n;i++){
			if(nums[i]==(nums[n]/3)*2){
				ans+=point;
			}
			if(nums[i]==nums[n]/3){
				point++;
			}
			
		}
		cout<<ans;
	}
}

有一个需要注意的点就是下面这两个if判断语句的顺序不能颠倒,可以试想一种情况(0,0,0,0,0,0,0),一个数组全为0的话,如果将顺序颠倒就会出现一种情况:只分成了两个数组,而第三个是空的。