NOIP2025补题笔记

· · 算法·理论

越菜越练。

A

一眼贪,注意到整组的买一定只买 x+y 最小的那一组。

对于只买 x 的,排序然后枚举前缀和即可。

::::success[AC Code]

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=100005;
int n;ll m,res;
ll a[N],b[N];
int main(){
  ios::sync_with_stdio(0);
  cin.tie(nullptr);
  cin>>n>>m;
  for(int i=1;i<=n;i++)cin>>a[i]>>b[i],b[i]+=a[i];
  sort(b+1,b+1+n),sort(a+1,a+1+n);
  for(int i=0;i<=n;i++){
    if(i)a[i]+=a[i-1];
    if(a[i]<=m)res=max(res,(m-a[i])/b[1]*2+i);
  }
  cout<<res;
  return 0;
}

::::

B

启示

特殊性质 m=2 启示了我们什么?

如果第一个是 w_x=1,后面被卡住的第一个 w_y=2,买的最后一个是 w_z=1,满足 a_z+a_x<a_y 则会爆炸。

正解

扩展到整体。

已经花了 m-1,然后第一个被卡住的是 u,最后买下的是 v,则如果前面买的存在一个 x,满足 w_x=1a_v+a_x<a_u 则不优。

因此容斥算不合法方案。

枚举 u,我们可以将其划分为三个部分。

分别称为 ABC

可以证明 v 一定在 C 中。

C 中枚举 v

此时可以确定可能会使策略爆炸的部分,满足 a_i<a_u-a_v,记为 D

如何使 AB 中在前考虑的部分满足 \sum w_i=m-1

$$num1={{|A|+|B|}\choose{m-1-|A|}}$$ 如何对爆炸的策略计数? 这是困难的,所以考虑在这里再容斥一次,求满足条件的数目。 当 $i$ 在 $D$ 中也在 $A$ 或 $B$ 中,那么其 $w_i$ 必须为 $2$。 因此对原式做调整。 设 $E=A\cup B$。 $$num2={{|A|+|B|-|D\cap E|}\choose{m-1-|A|-|D\cap A|}}$$ 后面的 $C$ 中,$i>v$ 的 $w_i=2$ ,$i<v$ 随便玩了,设这个数为 $k_{u,v}

所以答案就出来了。

ans=2^n-\sum_u \sum_v 2^{k_{u,v}}({{|A|+|B|}\choose{m-1-|A|}}-{{|A|+|B|-|D\cap E|}\choose{m-1-|A|-|D\cap A|}})

完结。

::::success[AC Code]

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=5005;
const int mod=998244353;
ll qpow(ll x,int y){
  ll res=1;
  while(y){
    if(y&1)res=res*x%mod;
    x=x*x%mod,y>>=1;
  }return res;
}
struct Set{int bg,ed,siz;}S[10];
int n,m,a[N],t[N];
ll fac[N],ifac[N];
inline ll C(int n,int m){
  if(m>n||n<0||m<0)return 0;
  return fac[n]*ifac[m]%mod*ifac[n-m]%mod;
}
inline int getJ(Set x,Set y){//取两集之交大小
  int l=max(x.bg,y.bg),r=min(x.ed,y.ed);
  return max(0,r-l+1);
}
void solve(){
  cin>>n>>m;ll res=0;
  for(int i=1;i<=n;i++)cin>>a[i];
  n++,a[n]=0;
  sort(a+1,a+1+n);
  for(int u=n;u>=2;u--){
    S[1]={u+1,n,n-u};
    int re=u-1,j=1;
    while(a[re]*2>a[u])re--;
    S[2]={re+1,u-1,(u-1)-(re+1)+1};
    S[3]={1,re,re};
    ll num1=C(S[1].siz+S[2].siz,m-S[1].siz-1);
    if(num1==0)continue;
    for(int v=S[3].ed;v>=1;v--){
      while(j<=n&&a[u]-a[v]>a[j])j++;
      S[5]={1,j-1,j-1};
      int sizE=getJ(S[5],S[1])+getJ(S[5],S[2]);
      ll num2=C(S[1].siz+S[2].siz-sizE,m-S[1].siz-1-getJ(S[1],S[5]));
      int sizF=max(v-2,0);
      ll num=(num1-num2+mod)%mod*t[sizF]%mod;
      res=(res+num)%mod;
    }
  }
  res=(t[n-1]+mod-res)%mod;
  cout<<res<<"\n";
}
int main(){
  ios::sync_with_stdio(0);
  cin.tie(nullptr);
  fac[0]=ifac[0]=t[0]=1;
  for(int i=1;i<N;i++)fac[i]=fac[i-1]*i%mod,t[i]=t[i-1]*2%mod;
  ifac[N-1]=qpow(fac[N-1],mod-2);
  for(int i=N-2;i>=1;i--)ifac[i]=ifac[i+1]*(i+1)%mod;
  int CC,TC;cin>>CC>>TC;
  while(TC--)solve();
  return 0;
}

::::

C

解法1

首先有个暴力 dp

f_{u,i,j}u 点及其子树 \operatorname{mex}=i,由 j 个点权 \ge i

有树上背包做法,具体的,一维钦定最大值,一维背包转移,然后从后往前递推即可做到 O(n^3),有 48

::::success[n^3 Code]

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=405;
const ll inf=1e15;
inline void gmax(ll &x,ll y){x=max(x,y);}
int n,m,fa[N],siz[N];
ll f[N][N][N],tmp[N],t[N][N];
vector<int> G[N];
void dfs(int u){
  f[u][0][1]=0,f[u][1][0]=0,siz[u]=1;
  for(auto v:G[u]){
    dfs(v);
    for(int i=0;i<=siz[u]+siz[v];i++)
      for(int j=0;j<=siz[u]+siz[v];j++)t[i][j]=-inf;
    for(int i=0;i<=siz[u];i++)tmp[i]=-inf;
    for(int vmx=0;vmx<=siz[v];vmx++){
      for(int i=0;i<=siz[u];i++)tmp[i]=max(tmp[i],f[u][vmx][i]);
      for(int i=0;i<=siz[u];i++)
        for(int j=0;j<=siz[v];j++)
          gmax(t[vmx][i+j],tmp[i]+f[v][vmx][j]);
    }
    for(int i=0;i<=siz[v];i++)tmp[i]=-inf;
    for(int umx=0;umx<=siz[u];umx++){
      for(int i=0;i<=siz[v];i++)tmp[i]=max(tmp[i],f[v][umx][i]);
      for(int i=0;i<=siz[u];i++)
        for(int j=0;j<=siz[v];j++)
          gmax(t[umx][i+j],f[u][umx][i]+tmp[j]);
    }
    siz[u]+=siz[v];
    for(int i=0;i<=siz[u];i++)
      for(int j=0;j<=siz[u];j++)f[u][i][j]=t[i][j];
  }
  for(int i=0;i<siz[u];i++)
    for(int j=siz[u];j>=1;j--)
      gmax(f[u][i+1][j-1],f[u][i][j]);
  for(int i=0;i<=siz[u];i++)
    for(int j=0;j<=siz[u];j++)
      f[u][i][j]+=i;
}
void solve(){
  cin>>n>>m;
  for(int i=1;i<=n;i++)
    for(int j=0;j<=n;j++)for(int k=0;k<=n;k++)
      f[i][j][k]=-inf;
  for(int i=2;i<=n;i++)
    cin>>fa[i],G[fa[i]].push_back(i);
  dfs(1);
  ll ans=0;
  for(int i=0;i<=n;i++)
    for(int j=0;j<=n;j++)
      gmax(ans,f[1][i][j]);
  cout<<ans<<"\n";
  for(int i=1;i<=n;i++)G[i].clear();
}
int main(){
  // freopen("x.in","r",stdin);
  // freopen("x.out","w",stdout);
  ios::sync_with_stdio(0);
  cin.tie(nullptr);
  int TC;cin>>TC;while(TC--)solve();
  return 0;
}

::::

解法2

解法一启发我们想单个点的贡献。

如果以取到 \max 的点为儿子进行剖分,u 的贡献就是 u 到根的路径上最长的连续链。

则问题变成了对树的剖分形态进行 dp,有状态 f_{u,i,j} 表示 u 点目前处在的链长 j,历史最长链长为 i 的子树贡献,转移显然,是 3d-0d 的,可以做到 O(nm^2) 拿到 76 分。

::::success[nm^2 Code]

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=8005;
const int M=805;
const ll inf=1e13;
int n,m,fa[N],dep[N],siz[N];
ll s[N][M];
vector<ll> f[N][M];
vector<int> G[N];
void dfs(int u){
  dep[u]=dep[fa[u]]+1,siz[u]=1;
  for(auto v:G[u]){
    dfs(v);siz[u]+=siz[v];
    for(int i=1;i<=m;i++)s[u][i]+=f[v][i][1];
  }
  for(int i=1;i<=m;i++)
    for(int j=1;j<=m;j++)
      f[u][i][j]=i*siz[u];
  for(auto v:G[u]){
    for(int i=1;i<=m;i++)
      for(int j=1;j<=m;j++)
        if(j+1<=m)f[u][i][j]=max(f[u][i][j],i+s[u][i]-f[v][i][1]+f[v][max(i,j+1)][j+1]);
  }
}
void solve(){
  cin>>n>>m;m++;
  memset(s,0,sizeof s);
  for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)f[i][j].resize(m+1);
  for(int i=2;i<=n;i++)
    cin>>fa[i],G[fa[i]].push_back(i);
  dfs(1);
  cout<<f[1][1][1]<<"\n";
  for(int i=1;i<=n;i++)G[i].clear();
}
int main(){
  // freopen("x.in","r",stdin);
  // freopen("x.out","w",stdout);
  ios::sync_with_stdio(0);
  cin.tie(nullptr);
  int TC;cin>>TC;while(TC--)solve();
  return 0;
}

::::

解法3

这个 dp 的贡献分为两部分,一部分是当前链贡献,一个是历史链贡献,于是只需要分别维护这两个即可。

F_{u,i}=f_{u,1,i}G_{u,i}=f_{u,i,i}

::::success[AC Code] ```cpp #include<bits/stdc++.h> using namespace std; #define ll int #define lowbit(x) ((x)&(-x)) const int N=8005; const int M=805; int n,m,tim,dfn[N],siz[N],fa[N],dep[N]; ll F[N][M],G[N][M],sum[M],d[M]; vector<int> to[N][M],rG[N]; struct BIT{ ll T[N]; inline void init(){memset(T,0,sizeof T);} inline void add(int x,ll y){for(int i=x+1;i<N;i+=lowbit(i))T[i]+=y;} inline ll sum(int x){int res=0;for(int i=x+1;i;i^=lowbit(i))res+=T[i];return res;} inline ll qry(int l,int r){return sum(r)-sum(l-1);} }B[M],S[M];//B是原点权值,S是儿子权值 inline void upd(int p,int x,ll y){ B[p].add(dfn[x],y),B[p].add(dfn[x]+siz[x],-y); S[p].add(dfn[fa[x]],y),S[p].add(dfn[fa[x]]+siz[fa[x]],-y); } void dfs1(int u){ dfn[u]=++tim,siz[u]=1,dep[u]=dep[fa[u]]+1; to[u][0].push_back(u); for(auto v:rG[u]){ dfs1(v); siz[u]+=siz[v]; for(int i=0;i<m;i++) for(auto s:to[v][i])to[u][i+1].push_back(s); } } void dfs2(int u){ for(int i=1;i<=m;i++)F[u][i]=G[u][i]=siz[u]*i; for(auto v:rG[u])dfs2(v); //G的转移 for(auto v:rG[u]) for(int i=1;i<=dep[u]+1;i++)upd(i,v,F[v][i]); for(int i=1;i<=dep[u];i++)sum[i]=0; for(auto k:rG[u]){ int v=k; for(int i=1;i<=dep[u];i++)sum[i]+=F[v][i]; } for(auto k:rG[u]){ int v=k; for(int i=1;i<=dep[u];i++)G[u][i]=max(G[u][i],i+sum[i]-F[v][i]+G[v][i+1]); } //F的转移 for(int i=1;i<=dep[u];i++) for(auto k:to[u][i]){ int v=k; F[u][i]=max(F[u][i],i*i+G[v][i+1]+S[i].sum(dfn[fa[v]])-B[i].sum(dfn[v])); } } void solve(){ cin>>n>>m;m++,tim=0; for(int i=1;i<=m;i++)B[i].init(),S[i].init(); for(int i=2;i<=n;i++) cin>>fa[i],rG[fa[i]].push_back(i); dfs1(1);dfs2(1); cout<<F[1][1]<<"\n"; for(int i=1;i<=n;i++)rG[i].clear(); for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)to[i][j].clear(); } int main(){ // freopen("x.in","r",stdin); // freopen("x.out","w",stdout); ios::sync_with_stdio(0); cin.tie(nullptr); int TC;cin>>TC;while(TC--)solve(); return 0; } ``` :::: updated on 12.12: 提供了一份不卡常且更短的实现。 ::::success[AC Code] ```cpp #include<bits/stdc++.h> using namespace std; const int N=8005; const int M=805; int F[N][M],G[N][M],tmp[M],n,m; int fa[N],dep[N],dfn[N],siz[N],tim; vector<int> rG[N],to[N]; inline void gmax(int &x,int y){x=max(x,y);} struct BIT{ int t[M][N]; void init(){memset(t,0,sizeof t);} inline int lowbit(int x){return x&-x;} inline void add(int p,int x,int y){for(int i=x;i<N;i+=lowbit(i))t[p][i]+=y;} inline int sum(int p,int x){int res=0;for(int i=x;i;i^=lowbit(i))res+=t[p][i];return res;} }F1,F2; void dfs(int u){ dep[u]=dep[fa[u]]+1,siz[u]=1; dfn[u]=++tim,to[u].push_back(u); for(auto v:rG[u]){ dfs(v),siz[u]+=siz[v]; for(auto s:to[v])to[u].push_back(s); } for(int i=1;i<=m;i++)F[u][i]=G[u][i]=siz[u]*i,tmp[i]=0; for(auto v:rG[u]){ for(int i=1;i<=dep[u];i++){ tmp[i]+=F[v][i]; F1.add(i,dfn[v],F[v][i]),F1.add(i,dfn[v]+siz[v],-F[v][i]); F2.add(i,dfn[u],F[v][i]),F2.add(i,dfn[u]+siz[u],-F[v][i]); } } for(auto v:rG[u]) for(int i=1;i<=dep[u];i++)gmax(G[u][i],i+tmp[i]-F[v][i]+G[v][i+1]); for(auto v:to[u]){ int dis=dep[v]-dep[u]; if(v==u||dis>dep[u])continue; gmax(F[u][dis],dis*dis+G[v][dis+1]+F2.sum(dis,dfn[fa[v]])-F1.sum(dis,dfn[v])); } } void solve(){ cin>>n>>m,m++,tim=0; for(int i=2;i<=n;i++) cin>>fa[i],rG[fa[i]].push_back(i); dfs(1); cout<<F[1][1]<<"\n"; memset(F,0,sizeof F),memset(G,0,sizeof G); F1.init(),F2.init(); for(int i=1;i<=n;i++)rG[i].clear(),to[i].clear(); } int main(){ ios::sync_with_stdio(0); cin.tie(nullptr); int TC;cin>>TC;while(TC--)solve(); return 0; } ``` :::: ## D ### 启发性解法 性质 DE 启发我们使用分治,使用 ST 表简单维护前缀最大最小,求以每个点开头或结尾且越过分界点的最大值,贡献就是一个前缀或后缀最大物。 ### 正解 发现以 $2^p$ 为分界长度的分割性质,$2^p+1\le len\le 2^{p+1}$ 的区间,其会跨过 $1$ 到 $2$ 个分界线,最优化问题可以算重,所以直接预处理所有即可。 对于询问,整块预处理,散块以下界减 $1$ 作为分割大小直接跑一遍就行了,时间复杂度 $O(n\log^2n+q\log n+qn)$。 ::::success[AC Code] ```cpp #include<bits/stdc++.h> using namespace std; #define ll long long #define ull unsigned long long const int N=50005; const ll inf=1e13; int n,q,lg[N]; ll s[N],mx[N][18],mi[N][18],tmp[N],B[18][N],PL[20],PR[20],res[18][18][N],ans[N]; inline ll qry1(int l,int r){ if(l>r)return -inf; int k=lg[r-l+1]; return max(mx[l][k],mx[r-(1<<k)+1][k]); } inline ll qry2(int l,int r){ if(l>r)return inf; int k=lg[r-l+1]; return min(mi[l][k],mi[r-(1<<k)+1][k]); } void solve(int id,int lc,int rc){ int len=PL[id]-1; for(int xmid=len;xmid<n;xmid+=len){ int l=max(xmid-(len<<1)+1,1),r=min(xmid+(len<<1),n); tmp[l-1]=-inf; for(int i=l;i<=xmid;i++){ tmp[i]=max(tmp[i-1],qry1(max(xmid+1,i+lc-1),min(r,i+rc-1))-s[i-1]); ans[i]=max(ans[i],tmp[i]); } tmp[r+1]=-inf; for(int i=r;i>xmid;i--){ tmp[i]=max(tmp[i+1],s[i]-qry2(max(i-rc,l-1),min(i-lc,xmid-1))); ans[i]=max(ans[i],tmp[i]); } } } int main(){ // freopen("x.in","r",stdin); // freopen("x.out","w",stdout); for(int i=2;i<N;i++)lg[i]=lg[i>>1]+1; cin>>n; for(int i=1;i<=n;i++) cin>>s[i],s[i]+=s[i-1],mi[i][0]=mx[i][0]=s[i]; for(int j=1;j<=17;j++)for(int i=0;i<=n-(1<<j)+1;i++) mx[i][j]=max(mx[i][j-1],mx[i+(1<<(j-1))][j-1]), mi[i][j]=min(mi[i][j-1],mi[i+(1<<(j-1))][j-1]); for(int i=1;i<=n;i++)for(int j=0;j<18;j++)B[j][i]=-inf; for(int i=1;i<=n;i++)B[0][i]=s[i]-s[i-1]; PL[0]=PR[0]=1; int c=1; for(int len=1;len<n;len<<=1,c++){ int L=len+1,R=min(len<<1,n); PL[c]=L,PR[c]=R; for(int xmid=len;xmid<n;xmid+=len){ int l=max(xmid-(len<<1)+1,1),r=min(xmid+(len<<1),n); tmp[l-1]=-inf; for(int i=l;i<=xmid;i++){ tmp[i]=max(tmp[i-1],qry1(max(xmid+1,i+L-1),min(r,i+R-1))-s[i-1]); B[c][i]=max(B[c][i],tmp[i]); } tmp[r+1]=-inf; for(int i=r;i>xmid;i--){ tmp[i]=max(tmp[i+1],s[i]-qry2(max(i-R,l-1),min(i-L,xmid-1))); B[c][i]=max(B[c][i],tmp[i]); } } }c--; for(int i=0;i<=c;i++){ for(int j=1;j<=n;j++)tmp[j]=-inf; for(int j=i;j<=c;j++) for(int k=1;k<=n;k++)tmp[k]=res[i][j][k]=max(tmp[k],B[j][k]); } int q;cin>>q; while(q--){ ll l,r; cin>>l>>r; int LL=-1,RR=-1; for(int i=0;i<=c;i++){ if(PL[i]>=l&&PR[i]<=r){ if(LL==-1)LL=i; RR=i; } } if(~LL)for(int i=1;i<=n;i++)ans[i]=res[LL][RR][i]; else for(int i=1;i<=n;i++)ans[i]=-inf; for(int i=0;i<=c;i++){ int p=min(PR[i],r)-max(PL[i],l); if(p>=0&&p!=PR[i]-PL[i]) solve(i,max(PL[i],l),min(PR[i],r)); } ull num=0; for(int i=1;i<=n;i++)num^=(ull)ans[i]*i; cout<<num<<"\n"; } return 0; } ``` ::::