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

植物收集

题面:

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

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

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

题解:

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

\(n^2\)做法(\(80\)pts):

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

\(n \log n\)做法(\(100\)pts):

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

证明:
考虑$m \rightarrow m+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\)有一个排列,现在需要求出这个序列中有多少个子区间是美丽的。

题解:

\(n^2\)做法(\(40\)pts):

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

\(n \log n\)做法(\(100\)pts):

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

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

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

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

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

  • 若使用模拟单调栈,注意边界和初始化。
  • 注意长度为一的子区间会被重复减去$4$次,所以最后输出的结果为$ans+3*n$
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) = cs_1cs_2c . . . cs_nc\),其中 c 是一个小写英文字母。
不难发现这个函数的作用就是在\(s\)的每一个空位插入一个小写英文字母,如\(f(c, aba) =cacbcac\)
\(Z\)通过这个函数构造了使用了\(n\)个小写字母\(c_1, c_2 . . . c_n\)构造\(n\)个字符串\(s_1, s_2, . . . s_n\),其中 \(s_i = f(c_i, s_i−1)\),其中\(s_0\)空串。
\(Z\)也非常喜欢子序列,现在他想知道\(s_n\)中有多少个本质不同的非空子序列。

题解:

$\vert \sum \vert$表示字符集大小

\(2^n\)做法(\(50\)pts)

考虑dp,设$f_i$为前i个字符组成的非空子序列个数,$last_{i,c}$为i前面的上个字符c的位置,容易得出状态转移方程(c为当前字符,$last_c$数组为滚动数组): $$\begin{cases} f_i=f_{i-1}*2+1&last_c=0\\ f_i=f_{i-1}*2-f_{last_c-1}&last_c \neq 0\\ \end{cases}$$ 将字符串展开,dp计数即可。

\(n{\vert \sum \vert}^3\)做法:(\(100\)pts)

解法一中的方程难以优化,考虑重构dp方式,设$f_{i,c}$为前i个字符组成的以字符c结尾的非空子序列数量,由此推出转移方程($s_i$为位置i的字符): $$ \begin{cases} f_{i,c} = f_{i-1,c} & c \neq s_i\\ f_{i,c} = \sum_{j=1}^{\vert \sum \vert} f_{i-1,j}+1& c=s_i\\ \end{cases} $$ 该dp复杂度为$O(2^n\vert\sum\vert)$,考虑优化,发现该dp可写成矩阵形式,对角线变为一,转移矩阵中对应字母所在的列变为一: $$ \begin{bmatrix}0&0&\cdots&0&0&1\end{bmatrix}\times\begin{bmatrix}1&0&1&0&\cdots&0\\0&1&1&0&\cdots&0\\0&0&1&0&\ddots&0\\0&0&1&1&\ddots&0\\\vdots&\vdots&\vdots&\vdots&\ddots&\vdots\\0&0&1&0&\cdots&1\\\end{bmatrix}\times\begin{bmatrix}\cdots&\cdots&\cdots&\cdots&\cdots&\cdots\\\vdots&\vdots&\vdots&\vdots&\vdots&\vdots\\\ddots&\ddots&\ddots&\ddots&\ddots&\ddots\\\vdots&\vdots&\vdots&\vdots&\vdots&\vdots\\\cdots&\cdots&\cdots&\cdots&\cdots&\cdots\\\end{bmatrix}\times\cdots $$

观察发现所得字符串对称,设$T_i$为从操作i执行到操作n获得的字符串,发现$T_i=T_{i+1}+s_i+T_{i+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不会)