Living-Dream 系列笔记 第12期

Posted on 2024-03-09 12:27  _XOFqwq  阅读(2)  评论(0编辑  收藏  举报

本期主要讲解一维前缀和技巧。

知识点

我们令 \(a_i\) 表示原数组的第 \(i\) 个元素,则 \(sum_i\) 表示 \(a_i\)\(i\) 个元素之和,即:

\[sum_i=\sum^{i}_{j=1} a_j \]

我们知道,\(a\) 数组前 \(i\) 个元素的和 \(=\)\(i-1\) 个元素的和 \(+ a_i\)。于是便可得到 \(sum\) 数组的递推式:

\[sum_i=sum_{i-1}+a_i \]

特别的:

\[sum_0=0,sum_1=a_1 \]

前缀和技巧的使用场景一般为反复多次静态区间的元素和。

若指定一个静态区间 \(a\) 的左端点 \(L\) 和右端点 \(R\),求该区间的元素和,则有公式:

\[\sum_{L \le i \le R} a_i = sum_R-sum_{L-1} \]

例题

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;
}