NOIP 模拟赛(10.10):植物收集,美丽子区间,字符序列

植物收集

题面:

Dr. Wang是一位植物领域的专家。他要给他的学生们上一节课。课堂上需要展示一种植物。众所周知,植物的生长是有阶段的,本着严谨科学的态度,Dr. Wang 希望可以在课堂上给学生们展示该植物的每个生长阶段。
Dr. Wang要讲授的植物有n个阶段,现在他需要弄到该植物每种阶段各一株。他打听到了这种植物每个生长阶段的价格。但由于科研经费不足,有时候直接购买并不是一个好选择。所以他计划用上他的催熟科技。具体的,Dr. Wang可以进行如下两种操作:

  • ai的价格购买一株生长到第i个阶段的植物。
  • 花费k的代价使用催熟科技,将所有已购买的植物生长阶段增加1

若一株植物已经到了阶段n,则返回阶段1可以理解为成熟到只剩种子,然后被重新种下去了)。现在Dr. Wang想让你帮忙求出,他最少需要花费多少代价,可以收集到植物的每个生长阶段。

题解:

因为阶段n的植物会返回阶段1,所以破环为链,下文所有n均视为2n

n2做法(80pts):

观察发现,经过k次操作二后,阶段i植物的价值可看作:minj=ikiaj,可使用st表维护区间最小值,然后枚举操作二的次数,取min即可。

nlogn做法(100pts):

n2做法中枚举的最终代价输出,注意到代价是一个关于k的单谷函数,三分即可。

证明:
考虑mm+1时,每轮购买植物能节省的钱是单调不增的,但每轮使用科技都是增加k,所以二者相加必定会存在峰值,且两个方向均单调不增。
Code:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
#define ll long long
#define inf 0x7f7f7f7f
int n,k,a[N<<1],st[N<<1][21],lg[N<<1],l,r,n1;
ll ans;
void init(){
  memset(st,inf,sizeof(st));
  for(int i=1;i<=n;i++)lg[i]=lg[i-1]+(1<<lg[i-1]==i);
  for(int i=1;i<=n;i++)st[i][0]=a[i];
  for(int j=1;(1<<j)<=n;j++){
    for(int i=1;i+(1<<j)-1<=n;i++){
      st[i][j]=min(st[i][j-1],st[i+(1<<(j-1))][j-1]);
    }
  }
}
ll check(int x){
  ll res=0;
  for(int i=x+1;i<=n1+x;i++){
    int p=lg[x+1]-1;
    res+=min(st[i-x][p],st[i-(1<<p)+1][p]);
  }
  return res;
}
int main(){
  ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
  // freopen("collect.in","r",stdin);
  // freopen("collect.out","w",stdout);
  cin>>n>>k;
  l=0,r=n-1,n1=n;
  for(int i=1;i<=n;i++){
    cin>>a[i];
    a[i+n]=a[i];
    ans+=a[i];
  }
  n<<=1;
  init();
  while(l<r){
    int lmid=l+(r-l)/3,rmid=l+(r-l)/3*2;
    if(lmid==rmid)break;
    if(check(lmid)+1LL*k*lmid>check(rmid)+1LL*k*rmid)l=lmid;
    else r=rmid;
  }
  for(int i=l;i<=r;i++){
    ll t=check(i)+1LL*k*i;
    ans=min(t,ans);
  }
  cout<<ans;
  return 0;
}

美丽子区间

题面:

Z喜欢区间。
Z定义一个区间是美丽的,当且仅当这个区间的最大值或最小值不出现在区间的开头和结尾。如[3,2,4,1]不是一个美丽的区间,因为最小值1出现在了结尾;而[2,4,1,3]是一个美丽的区间,因为1,4没有出现在开头或结尾。
Z有一个排列,现在需要求出这个序列中有多少个子区间是美丽的。

题解:

n2做法(40pts):

st表预处理出所有区间最大值和最小值,暴力枚举所有长度大于等于4的子区间判断是否合法

nlogn做法(100pts):

考虑容斥,优美的定义是最大值和最小值都不出现在开头或结尾,这个条件难以限制,可以从容斥的角度考虑。优美区间的数量为:总区间数最大值在开头的子区间数量最小值在开头的子区间数量最大值在结尾的子区间数量最小值在结尾的子区间数量。

之后我们会发现如果一个子区间的最大值出现在开头且最小值出现在结尾,这样的子区间被我们减去了两次,出现了减多了的情况。因此我们需要额外加回来一些区间,即加上:最大值在开头且最小值在结尾的子区间数量+最大值在开头且最小值在结尾的子区间数量。

求最大值在开头的子区间数量,可以使用单调栈,求每个数右边第一个大于该数的位置;如对于pi,其右边第一个大于他的位置在ri,那么j[i,ri),子区间[ij]都是最大值在开头的子区间。最小值在开头的子区间等同理可求。

之后求最大值在开头且最小值在结尾的子区间数量,继续考虑单调栈;根据上面的分析,j[i,ri),子区间[i,j]都是最大值在开头的子区间,那需要求有多个j满足pj[i,j]的最小值。

不难发现这样的j,可以被一个从后往前加入元素的单调栈维护出来。只需要再单调栈中二分有多少个j满足j<ri即可

  • 若使用模拟单调栈,注意边界和初始化。
  • 注意长度为一的子区间会被重复减去4次,所以最后输出的结果为ans+3n
Code:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define inf 0x7f7f7f7f
#define ll long long
const int N=2e5+5;
int n,a[N];
int lmin[N],lmax[N],rmin[N],rmax[N];
int w[N],s[N],p,l,r;
void work(){
  memset(s,0,sizeof(s));
  a[n+1]=inf;p=1;s[p]=1;
  for(int i=2;i<=n+1;i++){
    if(a[i]<a[s[p]])s[++p]=i;
    else {
      while(a[i]>a[s[p]]&&p)rmax[s[p--]]=i;
      s[++p]=i;
    }
  }
  memset(s,0,sizeof(s));
  a[n+1]=0;p=1;s[p]=1;
  for(int i=2;i<=n+1;i++){
    if(a[i]>a[s[p]])s[++p]=i;
    else {
      while(a[i]<a[s[p]]&&p)rmin[s[p--]]=i;
      s[++p]=i;
    }
  }
  memset(s,0,sizeof(s));
  a[0]=inf;p=1;s[p]=n;
  for(int i=n-1;i>=0;i--){
    if(a[i]<a[s[p]])s[++p]=i;
    else {
      while(a[i]>a[s[p]]&&p)lmax[s[p--]]=i;
      s[++p]=i;
    }
  }
  memset(s,0,sizeof(0));
  a[0]=0;p=1,s[p]=n;
  for(int i=n-1;i>=0;i--){
    if(a[i]>a[s[p]])s[++p]=i;
    else{
      while(a[i]<a[s[p]]&&p)lmin[s[p--]]=i;
      s[++p]=i;
    }
  }
}
int main(){
  ios::sync_with_stdio(false);
  cin.tie(0),cout.tie(0);
   //freopen("interval.in","r",stdin);
   //freopen("interval.out","w",stdout);
  cin>>n;
  ll ans=1LL*n*(n+1)/2;
  for(int i=1;i<=n;i++)cin>>a[i];
  work();
  for(int i=1;i<=n;i++)ans-=rmin[i]+rmax[i]-lmin[i]-lmax[i];
  memset(s,0,sizeof(s));
  p=1,s[p]=n;
  for(int i=n-1;i>0;i--){
    if(a[i]>a[s[p]]){
      s[++p]=i;
      l=1,r=p;
      while(l<r){
        int mid=(l+r)>>1;
        if(s[mid]>rmax[i])l=mid+1;
        else r=mid;
      }
      ans+=p-r;
    }else{
      while(a[i]<a[s[p]]&&p)--p;
      s[++p]=i;l=1,r=p;
      while(l<r){
        int mid=(l+r)>>1;
        if(s[mid]>rmax[i])l=mid+1;
        else r=mid;
      }
      ans+=p-r;
    }
  }
  memset(s,0,sizeof(s));
  p=1,s[p]=1;
  for(int i=2;i<=n;i++){
    if(a[i]>a[s[p]]){
      s[++p]=i;
      l=1,r=p;
      while(l<r){
        int mid=(l+r)>>1;
        if(s[mid]<lmax[i])l=mid+1;
        else r=mid;
      }
      ans+=p-r;
    }else{
      while(a[i]<a[s[p]])p--;
      s[++p]=i;l=1,r=p;
      while(l<r){
        int mid=(l+r)>>1;
        if(s[mid]<lmax[i])l=mid+1;
        else r=mid;
      }
      ans+=p-r;
    }
  }
  cout<<ans+3*n;
  return 0;
}

字符序列

题面:

Z喜欢字符串。
Z定义了一个奇妙的函数f(c,s),对于一个长度为n的字符串f(c,s)=cs1cs2c...csnc,其中 c 是一个小写英文字母。
不难发现这个函数的作用就是在s的每一个空位插入一个小写英文字母,如f(c,aba)=cacbcac
Z通过这个函数构造了使用了n个小写字母c1,c2...cn构造n个字符串s1,s2,...sn,其中 si=f(ci,si1),其中s0空串。
Z也非常喜欢子序列,现在他想知道sn中有多少个本质不同的非空子序列。

题解:

||表示字符集大小

2n做法(50pts)

考虑dp,设fi为前i个字符组成的非空子序列个数,lasti,c为i前面的上个字符c的位置,容易得出状态转移方程(c为当前字符,lastc数组为滚动数组): {fi=fi12+1lastc=0fi=fi12flastc1lastc0 将字符串展开,dp计数即可。

n||3做法:(100pts)

解法一中的方程难以优化,考虑重构dp方式,设fi,c为前i个字符组成的以字符c结尾的非空子序列数量,由此推出转移方程(si为位置i的字符): {fi,c=fi1,ccsifi,c=j=1||fi1,j+1c=si 该dp复杂度为O(2n||),考虑优化,发现该dp可写成矩阵形式,对角线变为一,转移矩阵中对应字母所在的列变为一: [00001]×[1010001100001000011000101]×[]×

观察发现所得字符串对称,设Ti为从操作i执行到操作n获得的字符串,发现Ti=Ti+1+si+Ti+1,由此从n至1逆序合并矩阵,并输出矩阵最后一行的和即可

Code:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n;string s;
struct sqr{
    long long a[30][30];
    void init(){
        for(int i=0;i<=26;i++)a[i][i]=1;
    }
    sqr(){
        memset(a,0,sizeof(a));
    }
    sqr operator*(const sqr &x)const{
        sqr z;
        for(int k=0;k<=26;k++){
            for(int i=0;i<=26;i++){
                for(int j=0;j<=26;j++){
                    z.a[i][j]=(z.a[i][j]+a[i][k]*x.a[k][j])%mod;
                }
            }
        }
        return z;
    }
}res;
sqr insert(int x){
    sqr f;
    f.init();
    for(int i=0;i<=26;i++)f.a[i][x]=1;
    return f;
}
int main(){
    //freopen("subseq.in","r",stdin);
    //freopen("subseq.out","w",stdout);
    cin>>n;
    cin>>s;
    s=' '+s;
    long long ans=0;
    res.init();
    for(int i=n;i>=1;i--)res=res*insert(s[i]-'a')*res;
    for(int i=0;i<26;i++)ans=(ans+res.a[26][i])%mod;
    cout<<ans;
    return 0;
}

结束喽(T4不会)

posted @   LISOP  阅读(202)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示