本期主要讲解一维前缀和技巧。
知识点
我们令 \(a_i\) 表示原数组的第 \(i\) 个元素,则 \(sum_i\) 表示 \(a_i\) 前 \(i\) 个元素之和,即:
我们知道,\(a\) 数组前 \(i\) 个元素的和 \(=\) 前 \(i-1\) 个元素的和 \(+ a_i\)。于是便可得到 \(sum\) 数组的递推式:
特别的:
前缀和技巧的使用场景一般为反复多次求静态区间的元素和。
若指定一个静态区间 \(a\) 的左端点 \(L\) 和右端点 \(R\),求该区间的元素和,则有公式:
例题
T1
模板题,不讲。
T2
挺有意思的一道题。其实很简单。
我们知道传送器仅可使用一次,所以我们选择序列中元素和最大的长度为 \(k\) 的区间的左端点处使用传送器即可。
因此维护一个前缀和,\(O(n)\) 地枚举起点 / 终点,利用前缀和算出时间,最后答案就是总时间 \(-\) 最大时间。
注意开 long long
。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,k,ans=-1e18;
int a[1000031],sum[1000031];
signed main(){
cin>>n>>k;
for(int i=1;i<=n-1;i++) cin>>a[i],sum[i]=sum[i-1]+a[i];
if(k==0){
cout<<sum[n-1]; return 0;
}
for(int i=1;i+k-1<=n-1;i++) ans=max(ans,sum[i+k-1]-sum[i-1]);
cout<<sum[n-1]-ans;
return 0;
}
T3
也挺有意思的一道题。
很容易想到枚举左右端点 \(l,r\),对于所有满足 \(\sum_{l \le i \le r} a_i \bmod 7 = 0\) 的区间长度取 \(\max\) 即为答案。时间复杂度 \(O(n^2)\),原地爆炸。
我们知道 \(\sum_{l \le i \le r} a_i = sum_r - sum_{l-1}\),若这个式子 \(\bmod \ 7=0\),则说明 \(sum_r \equiv sum_{l-1} \pmod 7\)。
而一个自然数 \(\bmod \ 7\) 的余数仅可能为 \(0 \sim 6\) 共 \(7\) 种可能,因此我们考虑枚举这 \(7\) 种可能,对于每一种可能将 \(a\) 数组分别正序和倒序扫描一遍,求出 \(\bmod \ 7\) 余数为当前余数的第一个 \(sum_i\) 和最后一个 \(sum_j\),区间 \([i+1,j]\) 便是对于当前余数的最大区间,对所有这样的区间长度取 \(\max\) 即可。
为避免溢出,需要在计算 \(sum\) 数组时先模上 \(7\)。
于是你写完了。
然后被 hack
了。
我们发现当余数为 \(0\) 时,\(l\) 最小可能为 \(0\),因此将内循环中枚举 \(j\) 的初值设为 \(0\) 即可。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,ans=-1;
int a[500031],sum[500031];
signed main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i],sum[i]=(sum[i-1]+a[i])%7;
//for(int i=1;i<=n;i++) cout<<sum[i]<<' ';
//cout<<'\n';
for(int i=0;i<7;i++){
int st,ed;
for(int j=0;j<=n;j++){ if(sum[j]==i){ st=j; break; } }
for(int j=n;j>=0;j--){ if(sum[j]==i){ ed=j; break; } }
ans=max(ans,ed-st);
}
cout<<ans;
return 0;
}
习题
T4
题目就是说要你改变最少的字符来实现字符串产生一边 \(0\) 一边 \(1\) 或者一边 \(1\) 一边 \(0\) 的局面。
要产生上述局面,则 \(0\) 和 \(1\) 的连续子串会有一个分界点,我们可以考虑枚举这个分界点。
同时,我们维护两个前缀和数组 \(z\) 和 \(o\),其中 \(z_i\) 和 \(o_i\) 分别表示前 \(i\) 的字符的 \(0\) 的个数和 \(1\) 的个数。
于是,对于一个分界点,利用前缀和数组快速求出前 \(i\) 个字符的 \(0\) 个数和后面字符的 \(1\) 个数,从而计算出还需要改变多少字符才能达成前面 \(1\) 后面 \(0\) 的局面,反之同理。
对于达成两种局面所需要改变的字符数取 \(\min\) 即可。
多测清空!多测清空!多测清空!
#include<bits/stdc++.h>
using namespace std;
int t,ans;
int sz,so,z[1031],o[1031];
string s;
int main(){
cin>>t;
while(t--){
sz=0,so=0;
memset(z,0,sizeof(z)),memset(o,0,sizeof(o));
cin>>s;
for(int i=0;s[i];i++){
z[i]=z[i-1],o[i]=o[i-1];
if(s[i]=='0') sz++,z[i]++;
else so++,o[i]++;
}
ans=1e9;
for(int i=0;s[i];i++){
ans=min(ans,z[i]+so-o[i]);
ans=min(ans,o[i]+sz-z[i]);
}
cout<<ans<<'\n';
}
return 0;
}
T5
求出 \(0 \sim 10^4\) 的是质数的 \(f(i)\),并累加入前缀和数组 \(q\) 中。
对于每次询问,输出 \(\dfrac{q_r-q_{l-1}}{b-a+1}+10^{-6}\) 即可,加上 \(10^{-6}\) 是因为题目卡精度。
#include<bits/stdc++.h>
using namespace std;
int l,r,n;
double ans,q[10031];
int f(int x){ return x*x+x+41; }
bool isp(int x){
if(x<2) return 0;
for(int i=2;i*i<=x;i++) if(x%i==0) return 0;
return 1;
}
void init(){
q[0]=1;
for(int i=1;i<=10000;i++) q[i]=q[i-1]+isp(f(i));
}
int main(){
init();
while(cin>>l>>r){
n=r-l+1,ans=q[r]-q[l-1];
cout<<setprecision(2)<<fixed<<ans*100.0/(double)n+1e-6<<'\n';
}
return 0;
}