NOIP 模擬賽(10.10):植物收集,美麗子區間,字元序列

LISOP發表於2024-10-14

植物收集

題面:

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}^n 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\)也非常喜歡子序列,現在他想知道 sn 中有多少個本質不同的非空子序列。

題解:

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