【题解】2022年第四场寒假集训营题解

视频讲题 https://www.bilibili.com/video/BV1JS4y1C7z5
视频讲题ppt下载
jiangly 1h 9min AK实况 https://www.bilibili.com/video/BV1534y117GQ

难度预计:

easy: E H K

medium: F I C D A J

medium-hard: G B

hard: L

实际难度:

EHKFCAJIDGBL

预计ak人数:15

实际ak人数:14

写在前面的话

本场比赛本来是准备放在第一场的,因不可抗因素不得不调整到第四场。难度总体偏低,除了最后一题以外严格意义上都不是特别难,很考验大家的知识点基本功,希望赛场中没过的同学也可以多多补题,提升自己的算法能力~

level.0 E 真假签到题

对标cf难度:800

知识点:数学,记忆化搜索(划掉)

签到题。直接粘贴函数的话会超时(复杂度为 )。但本质上函数为 ,所以输入什么就输出什么即可。

如果没看出来也没关系,可以通过找规律打表发现,或者直接套一个记忆化搜索(复杂度为 )都可以通过。

证明:

首先该函数是:

时,有 成立。

假设对于所有的 成立。

那么对于 而言, 由于此时 ,所以一定有

于是有

因此根据数学归纳法,对于所有

证毕。

参考代码(python):

print(input())

参考代码(记忆化搜索):

#include<bits/stdc++.h>
using namespace std;
map<long long,long long>mp;
long long f(long long x){
    if(x==1)return 1;
    if(mp[x])return mp[x];
    return mp[x]=f(x/2)+f(x/2+x%2);
}
int main(){
    long long x;
    cin>>x;
    cout<<f(x);
}

level.1 H 真真真真真签到题

对标cf难度:1000

知识点:立体几何,博弈

首先显然紫会选择中间。如果紫不选中间,那么小红选择离紫最远哪个角落一定比离中间要更远一些。

当紫选择了中间以后,小红必然选择任意一个角。

所以现在的问题变成了:已知正方体中心到顶点的距离为 ,求正方体的体积。

假设边长为,那么根据勾股定理有,所以计算出

最终输出边长的三次方即可。

建议:本题由于输出是浮点数,所以可以用pow函数。一般整数的乘方不建议用pow函数(容易出现精度问题),建议用for循环或者快速幂进行模拟。

参考代码:

#include<bits/stdc++.h>
using namespace std;
int main(){
    double x;
    cin>>x;
    x=x*2/sqrt(3);
    printf("%.7f",pow(x,3));
}

level.2 K 小红的真真假假签到题题

对标cf难度:1100

知识点:构造,位运算

本题要求构造一个数,为给定的数的倍数,并且二进制下包含的子串,并且 中'1'的数量和 中 '1'的数量不能相等。

如果没有最后一个条件,输出 即可。因为 的二进制后面添加一个 '0',显然满足前两个条件。

现在有了 '1' 数量不等的限制条件,那么就不能用上面的方式构造。

一个最简单的构造方式就是,将 的二进制重复写两次。显然满足子串条件和'1'不等条件。倍数条件也很容易证明:将 的二进制写两次等价于将 后面添加一些'0',再加上 。而第一步操作也就是乘上一个2的幂,显然一直都满足是的倍数这一条件。

参考代码(解法1):

#include<bits/stdc++.h>
using namespace std;
#define ll long long
int main(){
    ll x;
    cin>>x;
    int i;
    for(i=0;i<=32;i++)if(1LL<<i>x)break;
    cout<<x*(1LL<<i)+x;

}

当然还有个更简单的方式:由于 的二进制一定不超过30位(因为x不大于1e9),所以直接将 后面加30个'0'之后加x也可以,这样必然不会超过 的限制。

提示:灵活使用位运算运算符可以大大减少码量哦~另外位运算的运行速度在计算机底层是最快的,多用位运算可以减少代码的运行常数。

一些常用的位运算技巧:

if(x&1)... 等价于 if(x%2==1)... 判断x是否是奇数。

x>>=1 等价于 x/=2

x<<=1 等价于 x*=2

1<<x 等价于 2的x次方,常用于状压枚举、状压dp。

x&(-x) 为 x 的最低位的 '1' 对应的值,常用于树状数组。

l+r>>1 等价于 (l+r)/2,常用于二分。

其他的还有很多,就不一一列举了。

参考代码(解法2):

#include<bits/stdc++.h>
using namespace std;
#define ll long long
int main(){
    ll x;
    cin>>x;
    cout<<(x<<30)+x;
}

level.3 F 小红的记谱法

对标cf难度:1200

知识点:模拟

本题最大的难度是读题,题目读懂了其实很简单。

不难发现,'<' 代表降八度,'>'代表升八度。那么用一个变量统计当前八度的情况,然后对应哪个音直接输出即可,后面根据八度的情况来添加对应数量的'.'或者'*'即可。

注:之所以出这么长的题面也是为了让大家适应比赛,众所周知,xcpc的题面有很多又臭又长,而且是全英文题面,大家必须学会在读题的时候迅速找到关键信息并转化为解题的要素。

参考代码:

#include<bits/stdc++.h>
using namespace std;
int main(){
    int cnt=0,i;
    string s;
    cin>>s;
    for(i=0;i<s.length();i++){
        if (s[i]=='>')cnt++;
        else if(s[i]=='<')cnt--;
        else{
            if(s[i]=='C')cout<<1;
            if(s[i]=='D')cout<<2;
            if(s[i]=='E')cout<<3;
            if(s[i]=='F')cout<<4;
            if(s[i]=='G')cout<<5;
            if(s[i]=='A')cout<<6;
            if(s[i]=='B')cout<<7;
            if(cnt>0)for(int j=0;j<cnt;j++)cout<<'*';
            else for(int j=cnt;j<0;j++)cout<<'.';
        }
    }
    cout << endl;
}

level.3 I 爆炸的符卡洋洋洒洒

对标cf难度:1300

知识点:01背包

01背包的变种题。

传统01背包的模型是,每个物品有一个重量和一个价值,问总重量不超过 能取到的最大价值。

而这道题的模型变成了:总重量必须是 的倍数时能取到的最大价值。

解法其实是一样的,不同的是本来要维护前 个物品重量为 的最大值,现在变成了 前 个物品重量模 的最大值。这种技巧大家一定要熟练掌握。

转移方程:

这里要注意负数取模的技巧:先转为绝对值小于 的负数,然后加 再模

参考代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll a[1010][2],dp[1010][1010];
int main(){
    int n,k,i,j;
    cin>>n>>k;
    for(i=1;i<=n;i++)cin>>a[i][0]>>a[i][1];
    for(i=0;i<=n;i++)for(j=0;j<=k;j++)dp[i][j]=-1e16;
    //注意这里的初始化一定要尽可能小!代表是取不到的。
    dp[0][0]=0;
    //前0件物品,显然取到的最大威力为0。此时消耗魔力模p等于0
    for(i=1;i<=n;i++){
        for(j=0;j<k;j++){
            dp[i][j]=dp[i-1][j];
            //这一部分是不取第i个符卡。
            dp[i][j]=max(dp[i][j],dp[i-1][(j-a[i][0]%k+k)%k]+a[i][1]);
            //这一部分是取第i个符卡,那么目前的威力如果模k为j的话,一定是从“前i-1个符卡,威力模k为j-a[i]转移而来”
        }
    }
    if(dp[n][0]<=0)cout<<-1;
    else cout<<dp[n][0];
}

level.3 C 蓝彗星

对标cf难度:1300

知识点:差分,前缀和

给定每个彗星的种类,以及开始的时间。持续时间均为 。问有多少时间能看到蓝彗星看不到红彗星。

看到这个题面很容易想到差分的做法。我们最终的目的是想知道每个时刻有没有蓝彗星和红彗星,由于每颗彗星的时间是给定的一个区间,那么可以直接对值域进行差分,最终求一个前缀和即可。

在操作进行结束以后,我们需要对每个时刻进行枚举,统计只包含蓝彗星不包含红彗星的时刻数量。

请务必注意,本题的值域数组要开到2e5!因为如果最后一颗彗星在1e5的时刻出现,持续了1e5的时刻,那么将持续到2e5秒。只开1e5的话会发生段错误。

差分介绍:

一个数组,有多次操作,每次对某个区间 上每个数加上 ,问最终的数组是什么样子。

我们不妨假设初始的数组全是0。当我们对 区间上每个数加上 以后,这个数组就变成了 0,0,...,0,x,x,...,x,0,0,0

如果我们只对两个位置的数进行操作:,当我们做一次前缀和以后,我们发现,这个数组就变成了我们想要的样子。(前缀和是指,对于新数组 代表a数组中前i项之和。 数组可以通过 得出)。

因此我们可以先对每个区间修改只修改两个数:,在最后做一次前缀和就可以了。这就是差分的基本思想。

进阶:如果这道题, 的数据范围是 怎么做?(提示:离散化)

参考代码:

#include<bits/stdc++.h>
using namespace std;
int n,k;
string s;
int sum1[202020],sum2[202020];
int main(){
    int i,x;
    cin>>n>>k;
    string s;
    cin>>s;
    for(i=0;i<n;i++){
        cin>>x;
        if(s[i]=='B')sum1[x]++,sum1[x+k]--;
        else sum2[x]++,sum2[x+k]--;
    }
    int res=0;
    for(i=1;i<=2e5;i++)sum1[i]+=sum1[i-1],sum2[i]+=sum2[i-1],res+=(sum1[i]&&!sum2[i]);
    cout<<res;
}

level.4 D 雪色光晕

对标cf难度:1400

知识点:计算几何

本题要求一个固定的点到一条折线的最短距离。由于折线可以看成是很多条线段,所以可以规约成点到线段的最短距离。

(虽然这个是板子,但不建议直接去百度,毕竟赛场上没有百度)

显然,最终的答案一定为以下两种情况的一种:点到直线的最短距离、或点到线段某个端点的最短距离。

什么时候会遇到第二种情况呢?当且仅当点到直线的最短距离所对应的那个点不在线段上。

所以这道题一个最笨的方法就是:先根据线段求出直线两点式,然后求斜率乘积为-1(这样才能垂直)的直线,求出点斜式(还要特判斜率不存在的情况),这样求出两个直线交点,判断交点在不在线段上。如果在的话直接输出初始点到交点距离,否则输出初始点到线段两个端点的最小那个距离。

如果这道题用这种方法来做,计算量将非常大,题目难度也直接飙上1700+。事实上,本题有更简单的做法:

首先如何判断最终能不能取到点到直线距离呢?很简单,把点 和线段两个端点 ,这三个点连接成一个三角形,判断该三角形的角 和角 是否是钝角即可。判断钝角可以直接用勾股定理: 则角 为钝角。

然后如何求点到直线的距离呢?也很简单,到直线 的距离即三角形 边上的高。直接用 即可。而三角形面积可以直接用海伦公式:

可以发现,换一个做法,原本计算量巨大的题目将极大的减少做题的负担。在赛场上如果发现某题计算量非常大,不妨洗个脸冷静一下,换一个思路可能会豁然开朗。

*请注意,本题如果直接用网上的long long 存点的板子,可能会导致答案错误。因为在计算过程中会出现超过10^9的情况,这样在计算勾股定理的时候再一个平方就会出现爆long long精度的问题。解决办法要么使用__int128或者高精度,要么直接用double存点。 *

参考代码:

#include<bits/stdc++.h>
using namespace std;
struct point{
    double x,y;
    point(double x,double y):x(x),y(y){}
};
double dis(point a,point b){
    return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
double ar(point A,point B,point C){ //三点三角形面积
    double a=dis(B,C),b=dis(A,C),c=dis(A,B);
    double p=(a+b+c)/2;
    return sqrt(p*(p-a)*(p-b)*(p-c));
}
int jud(point A,point B,point C){       //判断角ABC是钝角
    double a=dis(B,C),b=dis(A,C),c=dis(A,B);

    return b*b>a*a+c*c;
}
double f(point a,point b,point c){  //c点到ab线段的最小距离
    double d1=dis(a,c),d2=dis(b,c);
    if(jud(c,a,b)||jud(c,b,a))
        return min(d1,d2);
    double s=ar(a,b,c);
    double d3=2*s/dis(a,b);
    return min(min(d1,d2),d3);
}
int main(){
    int n,i,j,k;
    double x,y,x0,y0;
    cin>>n;
    cin>>x0>>y0>>x>>y;
    point purple(x,y);
    point red(x0,y0);
    double res=1e16;
    for(i=0;i<n;i++){
        double xt,yt;
        cin>>xt>>yt;
        point temp(red.x+xt,red.y+yt);
        res=min(res,f(red,temp,purple));
        red=temp;
    }
    printf("%.8f",res);
}

level.4 A R

对标cf难度:1500

知识点:双指针,字符串

本题要求有多少子串包含至少 个 'R'字母,且不包含 'P'字母。

这里介绍两种方法解决本题。

方法一:正难则反

我们首先统计有多少个不包含'P'的字符串,然后这些字符串中会有一些是非法的(包含字符'R'数量少于个),把这些字符串的数量减去即可。

具体的操作如下(可结合代码进行理解):

1.统计不包含'P'的字符串数量(对于每个不含'P'的长度为 的子串,共可以贡献 个子串)

2.先减去不含 'R' 的数量

3.枚举正好有 个'R' 的情况,求子串数量。

总复杂度为

参考代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long

string s;
ll pr[202020],ed[202020];      //每个R前面有多少非R非P,后面有多少非R非P
ll sump[202020];
int main(){
    int i,j,k,n;
    cin>>n>>k;
    cin>>s;
    ll res=0,cnt=0;
    for(i=0;i<s.length();i++){//第一步 计算没有P的数量
        sump[i+1]=sump[i];
        if(s[i]!='P')cnt++;
        else{
            sump[i+1]++;
            res+=cnt*(cnt+1)/2;
            cnt=0;
        }
    }
    res+=cnt*(cnt+1)/2;
    cnt=0;
    for(i=0;i<s.length();i++){              //预处理每个'R'前缀的非'R'非'P'的数量
        if(s[i]!='R'&&s[i]!='P')cnt++;
        else {
            res-=cnt*(cnt+1)/2;         //减去长度为0的部分。
            if(s[i]=='R')pr[i]=cnt;
            cnt=0;
        }
    }
    res-=cnt*(cnt+1)/2;
    cnt=0;
    for(i=s.length()-1;i>=0;i--){         //预处理每个'R'后缀的非'R'非'P'的数量
        if(s[i]!='R'&&s[i]!='P')cnt++;
        else {
            if(s[i]=='R')ed[i]=cnt;
            cnt=0;
        }
    }
    vector<int>rs;          //存所有'R'的坐标
    for(i=0;i<s.length();i++){     
        if(s[i]=='R')rs.push_back(i);
    }
    int tong[1010]={};
    for(i=0;i<rs.size();i++){   //把小于k个'R'子串数量减去
        for(j=0;j<k-1;j++){
            if(i<rs.size()-j&&sump[rs[i+j]+1]==sump[rs[i]]){   //需要特判中间没有'P'
                tong[j]+=(pr[rs[i]]+1)*(ed[rs[i+j]]+1);
                res-=(pr[rs[i]]+1)*(ed[rs[i+j]]+1);
            }
        }
    }
    cout<<res;
}

方法二:化整为零

由于最终求的字符串不能包含字母'P',所以我们可以根据初始的'P'字符先将字符串切成很多小字符串。

然后问题就变成了,求一个字符串,有多少字符至少包含了 个 'R'。

这个问题如果枚举右端点,显然左端点是满足单调性的。可以用二分或者双指针来求出左端点的位置。

总复杂度为

参考代码:

#include<bits/stdc++.h>
using namespace std;
int n,k;
string s;
long long res=0;
void gao(string t){    //求字符串t中有多少子串包含至少k个'R'
    int i,j=0,temp=0;
    for(i=0;i<t.length();i++){//枚举右端点,找第一个不合法的左端点
        temp+=t[i]=='R';
        while(j<=i&&temp==k){
            temp-=t[j++]=='R';
        }
        //在这个左端点前面的都是合法的。
        res+=j;

    }
}
int main(){
    int i;
    cin>>n>>k>>s;
    string t="";
    for(i=0;i<n;i++){
        if(s[i]=='P')gao(t),t="";
        else t+=s[i];
    }
    gao(t);
    cout<<res;
}

level.4 J 区间合数的最小公倍数

对标cf难度:1500

知识点:数论

本题需要求区间所有合数的最小公倍数,也就是需要统计区间内每个素数出现的最高的幂。

这里再解释一下这句话的含义。例如,,那么它们的最小公倍数就是 ,也就是每个素数的幂取最大值。

由于本题的数据范围较小(本来是想出到1e7考线性筛的,但考虑到为了萌新,于是放过了 判素数的做法),因此可以直接暴力来做:枚举每个素数,找到落在该区间的最大因子即可。

复杂度

使用线性筛的话复杂度为 ,后面枚举素数因子和前面的做法是一样的,总复杂度为 ,其中 为不超过 的素数个数,约为 ,所以大概复杂度也是 级别。

参考代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
int mod=1e9+7;
int pr[101010];
int cnt=0;
int f(int x){
    int i;
    for(i=2;i*i<=x;i++)if(x%i==0)return 0;
    return 1;
}
int main(){
    int i;
    for(i=2;i<=30000;i++){
        if(f(i))pr[cnt++]=i;
    }
    int l,r,j;
    cin>>l>>r;
    ll res=1;
    for(i=0;pr[i]*2<=r;i++){
        for(j=pr[i];j<=r;j*=pr[i]){
            if(r/j==(l-1)/j)break;
        }
        res*=j/pr[i];
        res%=mod;
    }
    if(res==1)cout<<-1;
    else cout<<res;
}

level.5 G 子序列权值乘积

对标cf难度:1800

知识点:枚举,欧拉降幂

一个长度为 的数组,显然有 个非空子序列。如果我们去枚举每个子序列然后求其权值,复杂度将到达恐怖的 ,显然会超时。

我们可以观察到,当排序了以后,如果确定了最大值和最小值,它们中间的数取不取对结果是没有影响的。因此,通过这种方式可以枚举最大值和最小值,中间的部分规约在一起,利用欧拉降幂进行计算,这样复杂度为 ,这样还是不够。

我们看看这些指定了最大值和最小值的区间有什么特点呢?如果区间长度 ,那么中间的数有【取】或【不取】两种状态,因此有 的权值贡献。对于长度固定的情况,那么贡献的次数也就固定了。因此这部分可以利用乘法分配律合并起来。

这样我们最终只需要枚举区间长度就可以了。

举个例子,对于数组 [1,3,5,5,6] 而言(不妨设已经排好序了)。

我们枚举长度 2,最终带来的权值收益是

我们枚举长度 3,最终带来的权值收益是

我们枚举长度 4,最终带来的权值收益是

我们枚举长度 5,最终带来的权值收益是

根据公式 ,这些幂相同的底数都是可以合并的。合并之后我们发现,让枚举的长度变大的时候,它们的乘积是可以 进行维护的,因为只除掉了两个数。这部分可以用逆元解决。(如果枚举的长度从大到小,甚至不需要逆元)

那么最终的问题就是怎么解决计算 了。这就是这里要介绍的欧拉降幂:

根据费马小定理,当 互素时,有

那么我们要计算 的值,可以先把 写成 的形式,即求 ,前部分模 为1,所以答案就是 。v是什么?正是 取模的值。

也就是说,我们要求 ,如果 是个比较大的数(可能是个幂的形式),可以先让 取模,是不影响最终答案的正确性的(前提是 互素)。这个取模就叫做欧拉降幂。

参考代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll a[200010];
int mod=1e9+7;
ll power(ll a,ll b,ll mod){
    ll res=1;
    while(b){
        if(b&1)res=res*a%mod;
        b>>=1;
        a=a*a%mod;
    }
    return res;
}
ll inv(ll x,ll mod){
    return power(x,mod-2,mod);
}
int main(){
    int n,i;
    cin>>n;
    ll res=1,cnt=1,tmp=1;
    for(i=0;i<n;i++){ 
        cin>>a[i];
        res=res*a[i]%mod;
        tmp=tmp*a[i]%mod*a[i]%mod;
    }
    sort(a,a+n);
    ll l=0,r=n-1;
    while(l<n-1){
        tmp=tmp*inv(a[l++],mod)%mod*inv(a[r--],mod)%mod;
        res=res*power(tmp%mod,power(2,l-1,mod-1),mod)%mod%mod;
    }
    for(i=0;i<n;i++)res=res*a[i]%mod;
    cout<<res;
}

level.5 B 进制

对标cf难度:1900

知识点:线段树

这道题属于线段树的一个综合应用,考察大家对线段树原理的理解(如果只会抄板子想必是过不了的)。

首先给没接触过线段树、或者听说过但不了解线段树的同学介绍一下。线段树是一个比较特殊的数据结构。相比于普通暴力的 修改, 查询,或前缀和的 修改, 查询,线段树取了个折中,达成了 修改, 查询。这样就能解决那些一边修改一边查询的题目。

线段树的原理也比较好理解。它的本质其实是一颗二叉树,根节点代表了[1,n]区间。对于每个代表区间 的节点,它的左儿子代表 区间,右儿子代表 区间。其中

这样让我们修改数组中一个数的时候,我们将处理代表包含该数的区间的一系列节点。同理,当我们进行区间查询时,我们也需要查询一系列节点,找到每个节点对答案的贡献即可。

这道题的难点是进制查询,当我们在合并区间的时候,我们需要注意,假设对于 进制而言,左区间的权值为 ,右区间的权值为 ,那么左右区间合并后,权值应该变成 ,其中 代表右区间的长度。

那么这道题的思路就很清晰了,首先找到查询区间的最大值(用另一个线段树解决),来决定选用几进制。之后在对应的进制线段树上查询该区间的值即可。修改同理,当修改一个数的时候要处理它对所有包含该数的区间所造成的影响。

参考代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int long long
string s;
int mod=1e9+7;
ll power(ll a,ll b){
    ll res=1;
    while(b){
        if(b&1)res=res*a%mod;
        b>>=1;
        a=a*a%mod;
    }
    return res;
}
struct bigtree
{
    int l,r,ma;     //区间最大值
};

bigtree bt[410101];
void buildBT(int p,int l,int r){
    bt[p].l=l,bt[p].r=r;
    if(l==r){bt[p].ma=s[l-1]-'0';return;}
    int mid=l+r>>1;
    buildBT(p*2,l,mid);
    buildBT(p*2+1,mid+1,r);
    bt[p].ma=max(bt[p*2].ma,bt[p*2+1].ma);
}
int askma(int p,int l,int r){
    int res=0;
    if(bt[p].l>=l&&bt[p].r<=r){return bt[p].ma;}
    int mid=bt[p].l+bt[p].r>>1;
    if(mid>=l){
        res=max(res,askma(p*2,l,r));
    }
    if(mid+1<=r){
        res=max(res,askma(p*2+1,l,r));
    }
    return res;
}
void changeBt(int p,int id,int x){
    if(bt[p].l==id&&bt[p].r==id){
        bt[p].ma=x;
        return ;
    }
    int mid=bt[p].l+bt[p].r>>1;
    if(mid>=id){changeBt(p*2,id,x);}
    if(mid+1<=id){changeBt(p*2+1,id,x);}
    bt[p].ma=max(bt[p*2].ma,bt[p*2+1].ma);

}
struct jztree       //进制线段树
{
    int l,r;    
    ll res;
};
jztree t[400100][11];
void buildJZ(int p,int l,int r,int x){      //x进制线段树的构建
    t[p][x].l=l,t[p][x].r=r;
    if(l==r){t[p][x].res=s[l-1]-'0';return;}
    int mid=l+r>>1;
    buildJZ(p*2,l,mid,x);
    buildJZ(p*2+1,mid+1,r,x);
    int len=t[p*2+1][x].r-t[p*2+1][x].l+1;
    t[p][x].res=(t[2*p][x].res*power(x,len)%mod+t[2*p+1][x].res)%mod;
}
ll askans(int p,int l,int r,int x){    //查询x进制线段树中,l到r段的值。
    int res1=0,res2=0;
    if(t[p][x].l>=l&&t[p][x].r<=r){return t[p][x].res%mod;}
    int mid=bt[p].l+bt[p].r>>1;
    if(mid>=l){
        res1=askans(p*2,l,r,x);
    }
    if(mid+1<=r){
        res2=askans(p*2+1,l,r,x);
        int len=min(r,t[p*2+1][x].r)-mid;
        return (res1*power(x,len)+res2)%mod;
    }
    else return res1;
}
void addans(int p,int id,int k,int x){  //将x进制线段树上的id位改成k
    if(t[p][x].l==id&&t[p][x].r==id){
        t[p][x].res=k;
        return ;
    }
    int mid=t[p][x].l+t[p][x].r>>1;
    if(mid>=id){addans(p*2,id,k,x);}
    if(mid+1<=id){addans(p*2+1,id,k,x);}
    int len=t[p*2+1][x].r-t[p*2+1][x].l+1;
    t[p][x].res=(t[2*p][x].res*power(x,len)%mod+t[2*p+1][x].res)%mod;
}
signed main(){
    int n,m,i;    
    cin>>n>>m;

    cin>>s;
    buildBT(1,1,n);
    for(i=2;i<=10;i++){
        buildJZ(1,1,n,i);
    }
    while(m--){
        int op,l,r;
        cin>>op>>l>>r;
        if(op==1){
            changeBt(1,l,r);
            for(i=2;i<=10;i++)addans(1,l,r,i);
        }
        else{
            int ma=askma(1,l,r)+1;
            if(ma==1)ma++;
            cout<<askans(1,l,r,ma)%mod<<'\n';
        }

    }

}

level.6 L 在这冷漠的世界里光光哭哭

对标cf难度:2300

知识点:前缀和,二分

我们先假设这道题数据范围是 ,那么显然可以用 进行预处理:dp[i][j][k][h]代表前i个字符中,有多少子序列为第j、k、h个字母的字符串。这样后面每次查询就可以达成O(1)了。

但这样显然是过不了 的数据的,所以做法需要优化。

我们先做一个这样的前缀和,对于第 个字符是 ,字符串的前 个字符里,包含子序列 "ixj" 的数量是 。注意,这里的 "ixj" 中的 分别代表第 个字母和第 个字母。这样的预处理的复杂度是

之后,当我们想要查询区间 内有多少个子序列 abc 时,我们可以这样统计:

首先要把中间的字母 "b" 锁定在 区间。可以通过二分找到区间内第一个 "b"的位置的前一个坐标 和最后一个 "b" 的位置坐标 ,那么 即只包含 区间内的 的子序列 abc的数量。(注意dp方程里的a和c要对应成第几个字母的下标)。

之后我们需要减去不合法的部分。我们要减去的不合法情况有以下三种:

其中,中括号代表 区间。这样,我们还需要做到 查询长度为 2 的子序列。这个当然可以通过 复杂度的预处理做到:

当我们预处理出了 代表前个字符,包含子序列 "jk" 的数量以后,我们查询 中包含多少个 "ab"子序列只需要进行如下操作:

首先用 ,这样就计算出了前个字母比前 个字母多了多少"ab"。之后还需要减去 区间的"a"数量乘以 区间的 "b"数量,这代表了 "a" 落在前部分区间, "b" 落在中间区间的情况,最后就计算出了 区间中 "ab" 子序列的数量。

这样最终问题就得以解决了。总复杂度为

参考代码:

#include<bits/stdc++.h>

using namespace std;
#define ll long long
ll op[80020][26],sum2[80020][26][26],dp[80020][26][26],ed[80020][26],temp[26][26][26];
vector<int>ids[26];
ll check(int l,int r,int x,int y){
    return sum2[r][x][y]-sum2[l-1][x][y]-op[l-1][x]*(op[r][y]-op[l-1][y]);
}
int main(){
    ios::sync_with_stdio(false);cin.tie(0);
    int n,q,i,j,k,x,y,z;
    cin>>n>>q;
    //对于第k个字母是x,那么只取前k个字符x,ixj子序列的数量是dp[k][i][j]。
    //当询问[l,r]区间"abc"数量的时候,我们先用dp[最后一个b][a][c]-dp[第一个b前面的b][a][c],得到区间的b,配合全段a和c的子序列数量,然后减去取到l左边的a 或者r右边的c的情况。
    string s;
    cin>>s;
    for(i=0;i<26;i++)ids[i].push_back(0);
    for(i=1;i<=n;i++){
        ids[s[i-1]-'a'].push_back(i);
        for(j=0;j<26;j++)op[i][j]=op[i-1][j];
        op[i][s[i-1]-'a']++;
        for(j=0;j<26;j++){
            for(k=0;k<26;k++){
                sum2[i][j][k]=sum2[i-1][j][k];
            }
            sum2[i][j][s[i-1]-'a']+=op[i-1][j];
        }
    }
    for(i=n;i>0;i--){
        for(j=0;j<26;j++)ed[i][j]=ed[i+1][j];
        ed[i][s[i-1]-'a']++;
    }
    for(i=1;i<=n;i++){
        for(j=0;j<26;j++){
            for(k=0;k<26;k++){
                dp[i][j][k]=temp[s[i-1]-'a'][j][k]+=op[i-1][j]*ed[i+1][k];
            }
        }
    }
    while(q--){
        int l,r;
        string t;
        cin>>l>>r>>t;
        int x1=t[0]-'a',x2=t[1]-'a',x3=t[2]-'a';
        if(op[r][x2]-op[l-1][x2]==0){cout<<0<<'\n';continue;}
        int l1=lower_bound(ids[x2].begin(),ids[x2].end(),l)-ids[x2].begin();
        int r1=upper_bound(ids[x2].begin(),ids[x2].end(),r)-ids[x2].begin();
        long long res=dp[ids[x2][r1-1]][x1][x3]-dp[ids[x2][l1-1]][x1][x3];
        cout<<res-op[l-1][x1]*check(l,r,x2,x3)-check(l,r,x1,x2)*ed[r+1][x3]-op[l-1][x1]*(op[r][x2]-op[l-1][x2])*ed[r+1][x3]<<'\n';

    }

}
全部评论
给不懂的小白解释下兰子大佬这个条件的意思, if(r/j==(l-1)/j)break; 表示区间[l,r]内不存在j的倍数,必须要break,比如j=11,l =26,r=27的时候,如果不break,res*=(11*11)/11, 实际上应该为 res*=11/11。
3 回复 分享
发布于 2022-02-09 11:38
前排膜拜兰子哥
1 回复 分享
发布于 2022-02-08 19:00
orzorz
点赞 回复 分享
发布于 2022-02-08 21:21
j题能用用A∗B/gcd(A,B)A*B/gcd(A,B)A∗B/gcd(A,B)就是两数之积 / 最大公因数 算么
点赞 回复 分享
发布于 2022-02-11 16:31
if (x & 1) 怎么会和 if (x % 2 == 0) 等价呢,前者判断奇数后者判断偶数吧
点赞 回复 分享
发布于 2022-02-11 22:57
请问一下A题的第二种方法中的t=“ ”是什么意思还有就是咋把字符串拆开
点赞 回复 分享
发布于 2022-02-12 23:07
L题数据只有5000的时候,预处理dp[i][a][b][c]之后,怎么可以O(1)完成查询呀🤤
点赞 回复 分享
发布于 2022-02-13 11:35
作者:大鹏38 链接:https://ac.nowcoder.com/discuss/833710?type=101&order=3&pos=28&page=0&channel=-1&source_id=1 来源:牛客网 做题困难就来学,【课程】2021牛客竞赛算法入门秋季班开课啦!通过下面邀请链接报名,可立减20.0元. https://www.nowcoder.com/courses/cover/live/724?coupon=AsiMBN7 加QQ :61194610,发一个红包 牛客竞赛语法入门班 ,通过下面邀请链接报名,可立减10.0 https://www.nowcoder.com/courses/cover/live/678?coupon=AI6lOdI 加QQ :61194610,发一个红包
点赞 回复 分享
发布于 2022-02-13 16:00

相关推荐

和蔼:在竞争中脱颖而出,厉害! 但是有一个小问题:谁问你了?😡我的意思是,谁在意?我告诉你,根本没人问你,在我们之中0人问了你,我把所有问你的人都请来 party 了,到场人数是0个人,誰问你了?WHO ASKED?谁问汝矣?誰があなたに聞きましたか?누가 물어봤어?我爬上了珠穆朗玛峰也没找到谁问你了,我刚刚潜入了世界上最大的射电望远镜也没开到那个问你的人的盒,在找到谁问你之前我连癌症的解药都发明了出来,我开了最大距离渲染也没找到谁问你了我活在这个被辐射蹂躏了多年的破碎世界的坟墓里目睹全球核战争把人类文明毁灭也没见到谁问你了
点赞 评论 收藏
分享
66 27 评论
分享
牛客网
牛客企业服务