2026-04-01 赛后总结
拿到下发文件之后先通览全文,了解大意,概括文章内容。第一感觉是 \(T4\) 怎么又考期望,不会。然后感觉 \(T1\) 巨简单无比,求的是构造一个长为 \(n\) 的序列(\(a_i\) \(\in\) \([1,2*10^5]\))使得该序列可以满足 \(m\) 个形如 \(\oplus_{j=1}^{x_i}a_j=y_i\) 的条件。一眼晃就是把条件按 \(x\) 排序后将要构造的序列分成 \(m\) 段,每段分类讨论即可。差不多一个小时左右调完,因为写错了好多奇奇怪怪的东西,开始学习如何使用下发文件里的 \(spj\)。约莫学习了三十分钟,大概是会了,就开始测大样例。发现跑的贼拉慢,明明时 \(m\) \(log\) \(m\) 的啊(瓶颈在排序),没招了。调神秘的 \(TLE\) 花了一个多小时,关闭输入输出同步还是慢,加上快读依旧慢。非常神奇的是不加快读一秒多,加了要跑十多秒,何意味。调到最后力竭了放着没管,归功于编译器发神经了。
此时还剩两个小时,急速开 \(T2\),不会,扔进垃圾箱。看了看 \(T3\),是说求一棵树上恰好满足端点 \(u\) 是路径上的最大值或端点 \(v\) 是路径上的最小值两个条件中任意一个的点对 \((u,v)\) 有多少。第 \(i\) 次询问需要在节点 \(fa_i\) 下端添加点权为 \(n+i\) 的一个点。思考二十多分钟,思考结果是 \(Kruskal\) 重构树。哪里知道去年 \(T\_\_L\_X\) 讲的时候压根儿没写过,遂打开万能的、神通广大的、非常有用的 \(OI-Wiki\) 现场学习。学成归来只剩一个小时了,打了四十分钟代码调了二十分钟死活调不出来,没救了。最后没时间了只能怀着对代码和题目的悲愤忍痛提交。
有趣的是赛后 \(DiaoHantong\)、\(pengyirui\) 和 \(ZYXzyx123\) 居然都说他们过了 \(T3\),我还以为是什么人均题,出分了才发现全场只有他们三个过了(滑稽)。
最终得分:\(85pt\) \(+\) \(0pt\) \(+\) \(40pt\) \(+\) \(0pt\) \(=\) \(125pt\),\(rk16\)
\(P.S.\) \(T1\) 打假了是因为少了一个情况,愉快的挂分。考的简直就是螺旋升天。
正解:
- \(T1\):就是我说的那样,只不过特判一个 \(3\) \(\oplus\) \(2\) \(\oplus\) \(1\) \(=\) \(0\) 就过了。主要是判断各种情况很麻烦。
#include<bits/stdc++.h>
using namespace std;
char *p1,*p2,buf[100000];
#define nc() (p1==p2 && (p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++)
int read(){
int x=0,f=1;
char ch=nc();
while(ch<48||ch>57){
if(ch=='-') f=-1;
ch=nc();
}
while(ch>=48&&ch<=57) x=x*10+ch-48,ch=nc();
return x*f;
}
void write(long long x){
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
return;
}
struct node{
long long x,y;
};
node res[1000005];
bool cmp(node a,node b){
return a.x<b.x;
}
int main(){
// freopen("xor.in","r",stdin);
// freopen("xor.out","w",stdout);
int n=read(),m=read();
for(int i=1;i<=m;++i) res[i].x=read(),res[i].y=read();
sort(res+1,res+m+1,cmp);
for(int i=m;i>1;--i){
if(res[i].x==res[i-1].x&&res[i].y!=res[i-1].y){
write(-1);
return 0;
}
res[i].y^=res[i-1].y;
if((res[i].x-res[i-1].x)==1&&res[i].y==0){
write(-1);
return 0;
}
}
res[0].x=0;
for(int i=1;i<=m;++i){
if(res[i].x==res[i-1].x) continue;
int len=res[i].x-res[i-1].x;
if(len%2==0){
for(int j=1;j<=len-2;++j) write(1),putchar(' ');
write(res[i].y+1),putchar(' '),write(res[i].y^(res[i].y+1)),putchar(' ');
}
else{
if(res[i].y==0){
for(int j=1;j<=len-2;j++) write(1),putchar(' ');
write(2),putchar(' '),write(3),putchar(' ');
}
else{
for(int j=1;j<=len-2;++j) write(1),putchar(' ');
if(len!=1) write(1),putchar(' ');
write(res[i].y),putchar(' ');
}
}
}
for(int i=res[m].x+1;i<=n;++i) write(1),putchar(' ');
return 0;
}
-
\(T2\):
- 题意忘说了,就是给定一个长为 \(n\) 的序列 \(a\) 求一种操作方案可以通过每次将长度大于一的一个区间整体移动到一个数前面去这样的操作使得 \(a\) 最终变成 \(b\)。
- 正解就是先把 \(n \leq 3\) 以及 \(a\) 和 \(b\) 不完全相等的情况判掉。对于每一个 \(a_i\) 不等于 \(b_i\),找到右边第一个位置 \(j\) 满足 \(a_i\) 等于 \(b_j\),剩下的就会分成两种情况:
- \(j\) 不等于 \(n\),将 \(j\) 和 \(j+1\) 两位移动到 \(i-1\) 前面;
- \(j\) 等于 \(n\),又分两种情况,分别是:
- \(i\) 小于 \(n-1\),将 \(i\) 到 \(n-1\) 的数全部移到最后;
- 至于 \(i\) 等于 \(n-1\),手玩一下不难发现可以先执行操作 \((n-1,n-2,n-3)\),再执行操作 \((n-2,n,n-4)\)。
- 可以证明,总操作数不会超过 \(n\) 次,因为除最后一种操作外,每次都有至少一个数被安置在了正确的位置;而在最后一种操作中,我们花费两次操作放好了至少两个数。
-
\(T3\):没错其实就是 \(Kurskal\) 重构树,没什么好说的。算了还是说一下吧:直接建两棵 \(Kruskal\) 重构树,一颗维护满足第一个条件即最大值条件,另一颗维护第二个条件即最小值条件。不难发现一个点 \(u\) 为满足这种路径条件的端点时的情况数即分别为两棵重构树上该节点对应的除自己以外的子树大小。容斥一下就会发现答案就是大根树维护的所有子树大小之和加上小根树上所有子树大小之和(除去子树根节点)减去二倍既满足第一个条件又满足第二个条件的数量。动动脑子即可得添加一个比以往所有节点都大的节点 \(n+i\) 会增加 \(n+i-1\) 种情况,再减去小根数上新节点到根的距离,即减去 \(deep[new]\),我将贴上史山错误代码示例。
#include<bits/stdc++.h>
using namespace std;
vector<int> vec[1000005],max_tree[1000005],min_tree[1000005];
int n,m;
int fa[1000005],siz[1000005],p[1000005],deep[1000005],in[1000005],out[1000005],tot=0;
long long A=0,B=0,C=0;
void work1(int u){
siz[u]=1,in[u]=++tot;
for(auto v:max_tree[u]) work1(v),siz[u]+=siz[v];
A+=siz[u]-1,out[u]=tot;
}
void work2(int u){
siz[u]=1;
for(auto v:min_tree[u]) deep[v]=deep[u]+1,work2(v),siz[u]+=siz[v];
B+=siz[u]-1;
}
int query(int u){
int cnt=0;
while(u>0) cnt+=p[u],u-=u&-u;
return cnt;
}
void work3(int u){
C+=2ll*(query(out[u])-query(in[u]-1));
for(int i=in[u];i<=n;i+=i&-i) p[i]++;
for(auto v:min_tree[u]) work3(v);
for(int i=in[u];i<=n;i+=i&-i) p[i]--;
}
int getfa(int u){
if(fa[u]==u) return u;
return fa[u]=getfa(fa[u]);
}
int main(){
// freopen("furnace.in","r",stdin);
// freopen("furnace.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<n;i++){
int u,v;
scanf("%d%d",&u,&v);
vec[u].push_back(v),vec[v].push_back(u);
}
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=n;i++)
for(auto j:vec[i]){
int fa_i=getfa(i),fa_j=getfa(j);
if(i>j&&fa_i!=fa_j) max_tree[i].push_back(fa_j),fa[fa_j]=fa_i;
}
work1(n);
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=n;i>=1;i--)
for(auto j:vec[i]){
int fa_i=getfa(i),fa_j=getfa(j);
if(i<j&&fa_i!=fa_j) min_tree[i].push_back(fa_j),fa[fa_j]=fa_i;
}
for(int i=1;i<=n;i++) siz[i]=0;
deep[1]=1,work2(1),work3(1);
long long ans=A+B-C;
printf("%lld\n",ans);
scanf("%d",&m);
for(int i=1;i<=m;i++){
int fa_new;
scanf("%d",&fa_new);
deep[n+i]=deep[fa_new]+1,ans+=(n+i-1)-deep[n+i];
printf("%lld\n",ans);
}
return 0;
}
- \(T4\):神秘期望题,大概是说有一个玩意描述一个序列的交换操作,每次询问给出前 \(d\) 次操作是已知的,而后面的操作是未知的,求操作完之后的逆序对个数的期望。正解好像是树套树之类的东东,好像也可以 \(CDQ\) 维护三维偏序啥的(不会)。
总结:
- 死磕一道题这一块;
- 码力太强了,调一道题居然调了两个半小时,后面的题不要了吗大姐;
- 到底在磨蹭什么;
- 请打草稿,一定会有特殊情况的没这么简单;

浙公网安备 33010602011771号