2026暑假ZR内训
Day1(NOIP)
T1
以下描述全在模p意义下,容易发现,操作前后,a+b不变,那么就有不合法的一个充分条件,并且可以只用关注a的变化,设a+b=s,那么操作就变成从 (a,s-a) 到 (2a,s-2a) 或者 (2a-s,2s-2a),方便起见,我们让所有数同时乘上s的逆元,这样s就为1了,转化为a变成2a或2a-1,容易发现,k步会变成 \([2^ka-2^k+1,2^ka]\),考虑枚举k,然后判断c是否在这个区间即可。
T2
考虑n<=25的时候怎么做,直接枚举集合A,然后判断是否有元素和等于A的集合B,这个可以用桶来记录,如果A和B有交,同时减去交集即可,时间复杂度 \(O(2^n)\)。那么N>25时,考虑先贪心分配n-25个数,具体来说就是维护两个集合,从大到小枚举当前加入的数,哪个集合的和小就加哪边,最后可以得到一个较小的差,最多为 \(W\)。然后让最后剩下的25个数用前面的算法填补这个差值就行了,由于数据随机,所以找到这样子集的概率很大,删除的个数明显<=25。
T3
考虑有几种情况可以产生贡献。如果一个点有兄弟,那么这个点和兄弟无法区分,所以无法求出,那么只有独生子才能有贡献。如果一个点子树大小>k,那么可以通过询问父亲子树和自己子树的差来进行询问。如果一个点子树大小<=k,那么它和子树内的点也无法区分了,所以一定是叶子,并且它的叔叔数量一定>=k,并且他的所有叔叔的子树大小>k或者为叶子,这样可以通过询问爷爷的子树大小减去爷爷的所有儿子的和再减去叔叔的所有子树来知道当前节点。可以考虑树形dp,第一种情况的转移就是删掉这个节点的其他兄弟子树,第二种呢就是找到一个满足有子树的儿子节点,然后其他儿子如果子树大小>k就保留dp值,<=k就删成叶子,最后判断当前节点的儿子数量是否>=k。
Code
#include<bits/stdc++.h>
using namespace std;
namespace IO{
#define gc getchar
#define pc putchar
#define ps puts
inline int in(){
int x=0,f=1;
char c=gc();
for(;!isdigit(c);c=gc())if(c=='-')f=-1;
for(;isdigit(c);c=gc())x=x*10+(c-'0');
return x*f;
}
inline void out(int x){
if(x<0)pc('-'),x=-x;
if(x>=10)out(x/10);
pc(x%10+'0');
}
inline void outsp(int x){out(x),pc(' ');}
inline void outln(int x){out(x),ps("");}
}
using namespace IO;
const int N=8e5+5;
int t,n,k,siz[N],f[N];
vector<int> g[N];
void dfs(int u){
bool p=0;
for(int v:g[u])dfs(v),siz[u]+=siz[v]+1,f[u]+=f[v],p|=!f[v]&&siz[v];
f[u]+=(int)g[u].size()>=k&&p;
for(int v:g[u])if(siz[v]>=k)f[u]=max(f[u],f[v]+1);
}
int main(){
t=in();
while(t--){
n=in(),k=in();
for(int i=2;i<=n;i++)g[in()].push_back(i);
dfs(1),outln(f[1]);
for(int i=1;i<=n;i++)g[i].clear(),siz[i]=f[i]=0;
}
return 0;
}
T4
考虑dp,设\(f_i\)为当前答案,转移就是分类讨论,第一种是单独划分,那么就是\(f_{i-1} \times a_i\),因为对于i-1的每一种情况,都乘上了 \(a_i\),根据乘法分配律可得。第二种情况就是跟上一个合并,首先上一个数左移了一位,所以说贡献是\(f_{i-1}\times 10\),然后还要加上当前这一位,那么根据乘法分配律,就是当前这一位乘上上个数,枚举分割点,贡献是\(\sum_{j=0}^{i-2}f_j \times a_i\)显然可以前缀和优化。
得分
60+0+0+0=60,T1没模干净,T2赛时做法没打完,T3没来得及看,T4交错代码,交成T2的了,把原来的100分顶掉了(警告一次)。
Day2(IOI)
T1
先分析简化版,没有move操作,考虑根号分治,对于关键点数量<=B的直接暴力修改,>B直接每次询问的时候访问,每次询问访问n/B个,复杂度 \(O(n\sqrt n)\)。有move应该考虑分类讨论行列交点之类的,可以使用启发式合并。
T2
赛时解法
首先容易想到先拎出来一颗生成树,这样黑白点就知道了,然后再check非树边。考虑dfs建树,每次寻找一条出边,这个可以二分。这样可以在 \(n\log n\)次找到生成树。然后非树边的判断就可以每次断掉一条树边,并加入所有同色边,这样就可以考虑二分,每次断掉当前未确定集合的一个前缀与这两个集合的连边,最终最小的不连通的点就是答案。然后检验这个点向哪个点集有连边,可以将这个点前面的灰点向两个点集的边断开,然后每次剩下这个点向其中一个点集的连边,这样就很容易知道了。
T3
考虑如何忽略打乱,容易想到发送一个单调不降的序列,这样收到了就可以直接排序。所以有一个做法就是做前缀和,显然值域超标了。考虑将数通过固定种子随机映射,然后将映射后的每一个数拆位做类似前缀和状物,碰到0当前值加一,碰到1发出去,这样显然是单调的。发现这样值域就是 \(popcount_0\),期望是4n的,刚好卡到值域,如果>4n,就01反转,并在末尾打上标记,可能要特殊处理以下刚好卡到256的情况。
#include<bits/stdc++.h>
#include"parrots.h"
using namespace std;
static int now,p[70][260],c[260];
mt19937 rnd(31415926);
void out(bool x){
if(!x||now>=256)now++;
else send(now);
}
void encode(int n,int a[]){
for(int i=0;i<64;i++){
for(int j=0;j<=255;j++)p[i][j]=j;
shuffle(p[i],p[i]+256,rnd);
}
int cnt=0;
bool f=0;
for(int i=0;i<n;i++)cnt+=__builtin_popcount(p[i][a[i]]);
if(cnt<4*n)f=1;
now=0;
for(int i=0;i<n;i++)for(int j=0;j<8;j++)out((p[i][a[i]]>>j&1)^f);
if(f)for(int i=0;i<=10;i++)send(255);
}
bool query(){
if(now==256)return 1;
if(c[now]){c[now]--;return 1;}
now++;return 0;
}
void decode(int n,int l,int a[]){
for(int i=0;i<64;i++){
for(int j=0;j<=255;j++)p[i][j]=j;
shuffle(p[i],p[i]+256,rnd);
}
sort(a,a+l);
for(int i=0;i<=255;i++)c[i]=0;
for(int i=0;i<l;i++)c[a[i]]++;
bool f=0;
if(c[255]>10)c[255]-=10,f=1;
now=0;
for(int i=0;i<n;i++){``
int now=0;
for(int j=0;j<8;j++)if(query()^f)now+=1<<j;
for(int j=0;j<=255;j++)if(p[i][j]==now){output(j);break;}
}
}
得分
0+0+0=0,T1暴力没调出来,T2赛时没调出来,T3没时间写
Day3(IOI)
T1
考虑在模3意义下,显然进行一次操作,和是不变的,并且唯一不合法的方案是两项交换,所以每次我们可以找到一个和为0的连续段,感性理解,段之间是不会互相交换的,没有意义。而且,在段内,最多段长次就可以复原,应该是可以贪心的。考虑怎么再节省一次,这样子每个位置只会操作一次(我们设对i位置操作是对i和i+1操作),那么实际上就是给一个合法的拓扑序,那么就可以dp。设状态 \(f_i/g_i\) 表示i比i-1先/后操作,然后分类讨论i,i-1,i-2的先后顺序,并判断对i-1的操作是否合法就可以转移了(还有一个重要引理,因为操作之后两边的和不同,所以,只需要三个数就可以判断是否合法,然后具体判断的话就是判断对角线的两个数是否是相等的)。设x为前i位a的和等于b时a_i的值,y为前i-1位的和等于b时a_i的值,转移首先是 \(f_i\)的转移,就是判断i-1这一步是否合法,第一种是从 \(f_{i-1}\) 转移过来,那么就是时间i<i-1<i-2,那么做i-1之前,i变成了x,i-1没变,然后做i-1之后,i变成\(b_i\)。第二个是 \(f_i\) 从 \(g_{i-1}\) 转移过来,那么就是i<i-1,i-2<i-1,i-1是最后操作的,那么就考虑这一步,原来i这个位置是x,最后i变成 \(b_i\),i-1变成 \(b_{i-1}\)。接下来是 \(g_i\)的转移,就是判断i这一步是否合法,首先是从 \(g_{i-1}\)转移过来,时间就是i-2<i-1<i,那么做i-1之前,i没变,做完i-1之后,i-1会变成\(b_{i-1}\),i会变成y。最后是从\(g_{i-1}\)转移过来,时间就是i-2>i-1,i>i-1,那么操作前就是i和i-1都没变,操作完i变成y。
Code
#include<bits/stdc++.h>
#include"explore.h"
using namespace std;
const int N=5e5+5;
int a[N],b[N],s1[N],s2[N];
bool f[N],g[N];
int I(char c){return (c=='G')+2*(c=='B');}
bool calc(int x,int y,int z){return x==y||y!=z;}
int explore(int n,string s,string t){
for(int i=1;i<=n;i++)a[i]=I(s[i-1]),s1[i]=(s1[i-1]+a[i])%3;
for(int i=1;i<=n;i++)b[i]=I(t[i-1]),s2[i]=(s2[i-1]+b[i])%3;
int ans=n;
for(int l=1,r=1;r<=n;r++)if(s1[r]==s2[r]){
f[l]=g[l]=1;
for(int i=l+1;i<=r;i++){
int x=(s2[i]-s1[i-1]+3)%3,y=(s1[i]-s2[i-1]+3)%3;
f[i]=(f[i-1]&&calc(x,a[i-1],b[i]))||(g[i-1]&&calc(b[i],b[i-1],x));
g[i]=(g[i-1]&&calc(y,b[i-1],a[i]))||(f[i-1]&&calc(a[i],a[i-1],y));
}
ans-=f[r]||g[r],l=r+1;
}
return ans;
}
T3
有一个很简单的次数 \(n^2\) 做法,就是每次只询问两个点,但是这样太浪费了,没有用到可以同时问多个的性质,而且他的 \(\sum size\) 远大于 cnt。这就提醒我们,如果问很多个会怎么样。会有两种情况,有边没什么用,没边的话就可以直接得出这一集合两两的答案。如果大小很大的话,抽到有边的概率非常高。又因为这个图非常稀疏,那么我们就考虑随机一个小于根号的大小来问三万次,这样子不会爆大小,而且有合适的概率能将答案范围缩小很多,最后剩下没问出来的像第一个做法暴力即可。
得分
0+0+100,赛时T1写的贪心做法,T2不会。
浙公网安备 33010602011771号