VP NOI2023
一个月前的事情捏,因为今天刚好在摸鱼就想起来写写。
Day 1
开题,先总的过一遍,好像比较传统。
T1 基本上是一眼题了,简单容斥一下就可以解决。很快开始写,写好过了小样例。但是这个时候还没有大样例。而我对这份代码并不太自信,感觉一车细节,所以开拍。
果然 WA 了。然后开始艰难地调试,毕竟代码确实是有点长。调了不知道多久终于过拍了,一看时间已经过去将近两小时了。感觉不太妙。
开 T2,感觉有点神秘,估计要放弃正解。先写个暴力过 1,2,再手推了 3~7 测试点,感觉不能做了。想着能过 就行,但是不会。先过。
T3 稍微分析一下看出一个性质,然后发现前 9 个点都是暴力,先写过去。再看看特殊性质,链不难,转化一下成为线段树优化 DP。写写写,拿小样例测了一下是过了,不知道写对没有。
再回去看看 T2,对 打表发现是双阶乘,这么神奇。
剩下的时间什么都不会。估分 100+50+52=202,但是没地方测。
Day 2
开题,居然有字符串。
T1 看 很小感觉是乱搞题。先写了一个暴力,然后开始想一些奇妙的东西,比如 次 dijk 之类的。然后突然发现这个 dijk 的结果可以继承,那好像就成了?复杂度上界是 ,但应该多算了不少,能过的样子。写之,测一下极限数据,还比较稳,写个拍丢了。大概一个多小时的样子。
T2 字符串题,只好往我会的方向想。试试能不能用 SA,然后发现真的可以,用 SA 之后配一个 Manacher,加三次二维数点就解决了。但是 SA 我也不太会写啊,怎么办呢,硬写。凭着模糊的记忆,光写 SA 可能就写了四五十分钟,但还是写过去了。接下来就没什么好说的了,正常写数点,很快调过去了。
大概三个半小时了,看 T3,题目都没理解,但看数据范围肯定有 10 分暴力,说不定还能多搞点。但是不想写了。
这时又发现洛谷上有题和大样例了,把两天的代码都测一下,居然一点没挂。这样总分 100+50+52+100+100+(>=10)=(>=412),算上笔试过了队线。
这场 VP 有点码力爆发的意思,总代码量接近 15K(其中 Day 1 接近 10K),感觉状态还不错。
但过了有什么用呢,又去不了。NOIP 挂成什么都不知道,去了 NOI 估计也是挂分。以前还是练的太少太少了。只能说赶紧练,希望 24 年能够去见见世面。
下面是题解部分。
D1T1
给定方格平面上若干条线段,有水平线,竖直线和斜率为 的线,其中斜线不超过 条。求这些线经过的格子数目。
题解:答案可以表示成 水平线和竖直线的并 加上 斜线的并 减去 斜线与水平线,竖直线的交点数目。第一个值离散化后扫描线求,第二个值直接暴力,第三个值用 set 记一下就行。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=1e5+5; int test,n,m,q,x[N<<2],y[N<<2],X,Y,cnt1,cnt2,cnt3;ll ans,cnt; struct node{int x1,y1,x2,y2;}; bool operator <(const node&a,const node&b){return a.x1<b.x1;} bool cmp(node a,node b){return a.x1-a.y1==b.x1-b.y1?a.x1<b.x1:a.x1-a.y1<b.x1-b.y1;} vector<node> q1,q2,q3,q4,Q; set<pair<int,int> > S; ll sum[N<<4],tag[N<<4]; void modify(int p,int l,int r,int L,int R,int v){ if(l>=L&&r<=R){ if(v==1){++tag[p];sum[p]=y[r+1]-y[l];} else{--tag[p];if(!tag[p])sum[p]=(l==r?0:sum[p<<1]+sum[p<<1|1]);} return; } int mid=l+r>>1; if(L<=mid)modify(p<<1,l,mid,L,R,v); if(R>mid)modify(p<<1|1,mid+1,r,L,R,v); if(!tag[p])sum[p]=sum[p<<1]+sum[p<<1|1]; } int main(){ // freopen("color.in","r",stdin); // freopen("color.out","w",stdout); scanf("%d",&test); scanf("%d%d%d",&n,&m,&q); for(int i=1,op;i<=q;i++){ node tmp; scanf("%d%d%d%d%d",&op,&tmp.x1,&tmp.y1,&tmp.x2,&tmp.y2); if(op!=3)ans+=max(abs(tmp.x1-tmp.x2)+1,abs(tmp.y1-tmp.y2)+1); x[4*i-3]=tmp.x1;x[4*i-2]=tmp.x2;x[4*i-1]=tmp.x1+1;x[4*i]=tmp.x2+1; y[4*i-3]=tmp.y1;y[4*i-2]=tmp.y2;y[4*i-1]=tmp.y1+1;y[4*i]=tmp.y2+1; if(op==1){if(tmp.x1>tmp.x2)swap(tmp.x1,tmp.x2);q1.push_back(tmp);} if(op==2){if(tmp.y1>tmp.y2)swap(tmp.y1,tmp.y2);q2.push_back(tmp);} if(op==3)q4.push_back(tmp); }cnt=ans; sort(x+1,x+4*q+1);sort(y+1,y+4*q+1); X=unique(x+1,x+4*q+1)-x-1;Y=unique(y+1,y+4*q+1)-y-1; sort(q4.begin(),q4.end(),cmp); node xx; for(int i=0;i<q4.size();i++){ if(i==0)xx=q4[i]; if(i!=0&&q4[i].x1-q4[i].y1!=q4[i-1].x1-q4[i-1].y1) q3.push_back(xx),xx=q4[i]; if(i!=0&&q4[i].x1-q4[i].y1==q4[i-1].x1-q4[i-1].y1){ if(xx.x2<q4[i].x1)q3.push_back(xx),xx=q4[i]; else xx.x2=max(q4[i].x2,xx.x2),xx.y2=max(q4[i].y2,xx.y2); } if(i==q4.size()-1)q3.push_back(xx); } for(node a:q3)ans+=a.x2-a.x1+1; for(node a:q3)for(node b:q1) if(a.y1<=b.y1&&a.y2>=b.y2&& b.y1-a.y1+a.x1>=b.x1&&b.y1-a.y1+a.x1<=b.x2) S.insert({b.y1-a.y1+a.x1,b.y1}); for(node a:q3)for(node b:q2) if(a.x1<=b.x1&&a.x2>=b.x2&& b.x1-a.x1+a.y1>=b.y1&&b.x1-a.x1+a.y1<=b.y2) S.insert({b.x1,b.x1-a.x1+a.y1}); for(node a:q1){ a.x1=lower_bound(x+1,x+X+1,a.x1)-x; a.x2=lower_bound(x+1,x+X+1,a.x2+1)-x; a.y1=lower_bound(y+1,y+Y+1,a.y1)-y; a.y2=lower_bound(y+1,y+Y+1,a.y2)-y; Q.push_back({a.x1,a.y1,1,a.y2}); Q.push_back({a.x2,a.y1,-1,a.y2}); } for(node a:q2){ a.x1=lower_bound(x+1,x+X+1,a.x1)-x; a.x2=lower_bound(x+1,x+X+1,a.x2+1)-x; a.y1=lower_bound(y+1,y+Y+1,a.y1)-y; a.y2=lower_bound(y+1,y+Y+1,a.y2)-y; Q.push_back({a.x1,a.y1,1,a.y2}); Q.push_back({a.x2,a.y1,-1,a.y2}); } sort(Q.begin(),Q.end()); for(int i=1,p=0;i<=X;i++){ while(p<Q.size()&&Q[p].x1==i) modify(1,1,Y,Q[p].y1,Q[p].y2,Q[p].x2),++p; cnt-=sum[1]*(x[i+1]-x[i]); } printf("%lld\n",ans-cnt-S.size()); return 0; }
D2T1
在一棵 层的满二叉树上加入若干条祖先指向后代的边,边有边权。树边由儿子指向父亲。对所有满足 能到达 的有序点对 ,求 到 的最短路径长度和。
题解:注意到一个事实:如果 能到达不在 子树内的点 ,那 到 的最短路径就等于 沿着树边走到 的路径长加上 到 的最短路径长。所以只要求出每个点到它子树内点的最短路径,再乘一些容易求的系数即可。
要求 到它所有后代的距离,可以从它的两个儿子继承而来。也就是说,儿子到其子树内点的最短距离加上连接它们的边的长度,可以作为初始值加入优先队列。然后跑 Dijkstra。
时间有点久了,真的忘了。
点击查看代码
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=3e5+5,P=998244353; int n,m,a[N],rt,vis[N],dep[N]; ll dis[N],sum0,sum1,sum2,sum3,ans; int head[N],nxt[N<<1],ver[N<<1],val[N<<1],tot; void add(int u,int v,int w){ ver[++tot]=v;val[tot]=w; nxt[tot]=head[u];head[u]=tot; } map<pair<int,int>,int> M; priority_queue<pair<ll,int> > Q; void init(int u){ dis[u]+=a[rt];vis[u]=0; if(dis[u]<1e17)Q.push({-dis[u],u}); if(u*2<(1<<n))init(2*u),init(2*u+1); } void dfs(int u){ ++sum0;sum2=(sum2+(dep[u]-dep[rt]+P)%P)%P; if(dis[u]<1e17)sum1=(sum1+dis[u]%P)%P,++sum3; if(u*2<(1<<n))dfs(2*u),dfs(2*u+1); } void solve(int s){ rt=s;init(s);dis[s]=0;Q.push({0,s}); while(!Q.empty()){ int u=Q.top().second;Q.pop(); if(vis[u])continue;vis[u]=1; for(int i=head[u],v;i;i=nxt[i]) if(dis[v=ver[i]]>dis[u]+val[i]&&v>rt){ dis[v]=dis[u]+val[i]; Q.push({-dis[v],v}); } } sum0=sum3=1;sum2=sum1=0;dfs(2*s); ll sum4=sum0,sum5=sum1,sum6=sum2,sum7=sum3; sum0=sum3=1;sum2=sum1=0;dfs(2*s+1); ans=(ans+sum4*sum1%P+sum2*sum7%P +sum0*sum5%P+sum6*sum3%P)%P; if(s*2<(1<<n-1))solve(2*s),solve(2*s+1); } int main(){ // freopen("trade.in","r",stdin); // freopen("trade.out","w",stdout); memset(dis,0x3f,sizeof(dis)); scanf("%d%d",&n,&m); for(int i=2;i<(1<<n);i++){ scanf("%d",a+i); add(i,i/2,a[i]);dep[i]=(dep[i/2]+a[i])%P; } for(int i=1,u,v,w;i<=m;i++){ scanf("%d%d%d",&u,&v,&w); if(M.find({u,v})==M.end())M[{u,v}]=w; else M[{u,v}]=min(M[{u,v}],w); } for(auto x:M)add(x.first.first,x.first.second,x.second); solve(1); printf("%lld\n",ans); return 0; }
D2T2
给定字符串 , 次询问,每次给定 ,求满足 ,且 的字典序小于 的翻转的字典序。
题解:如何用 SA 刻画这个条件?不妨扩展一下,如果满足条件,则 的字典序也小于 的翻转的字典序。那么就可以把原字符串翻转一遍接在后面,然后跑 SA,只要对应的字符串在后缀数组中排在前面就行。
但是有一点问题,就是可能 等于 的翻转,但仍然排在前面。这时会发现构成了一个回文串,所以跑 Manacher。
列一下式子。 表示后缀 的排名, 表示后缀 的排名。 表示以 和 的中心为缝隙的回文串的最大长度。则 满足条件,当且仅当 且 。
对第二个条件考虑反面。容易转成二维数点,剩下的推导是容易的。对第一个式子,我选择分奇偶各进行一次。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int N=2e5+5; int test,T,n,q,a[N],ans[N];char s[N],t[N]; int c[N],x[N],y[N],sa[N],rk[N]; void SA(){ int m=300;n*=2; for(int i=1;i<=n;i++)x[i]=t[i],c[x[i]]++; for(int i=1;i<=m;i++)c[i]+=c[i-1]; for(int i=n;i>=1;i--)sa[c[x[i]]--]=i; for(int k=1;k<=n;k*=2){ int num=0; for(int i=n-k+1;i<=n;i++)y[++num]=i; for(int i=1;i<=n;i++)if(sa[i]>k)y[++num]=sa[i]-k; for(int i=1;i<=m;i++)c[i]=0; for(int i=1;i<=n;i++)c[x[i]]++; for(int i=1;i<=m;i++)c[i]+=c[i-1]; for(int i=n;i>=1;i--)sa[c[x[y[i]]]--]=y[i],y[i]=0; swap(x,y);x[sa[1]]=num=1; for(int i=2;i<=n;i++){ if(sa[i]<=n-k&&sa[i-1]<=n-k&& y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) x[sa[i]]=num; else x[sa[i]]=++num; } if(num==n)break;m=num; } for(int i=1;i<=n;i++)rk[sa[i]]=i; n/=2; } //sa[i]表示排名为i的后缀是s[sa[i]:n] void Manacher(){ int l=0,r=0; for(int i=1;i<n;i++){ if(i>=r){ int j=0; while(i-j>=1&&i+j+1<=n&&s[i-j]==s[i+j+1])++j; if(j>0){l=i-j+1;r=i+j;}a[i]=j; } else{ int p=l+r-i-1; if(a[p]<r-i)a[i]=a[p]; else{ int j=r-i; while(i-j>=1&&i+j+1<=n&&s[i-j]==s[i+j+1])++j; r=i+j;l=i-j+1;a[i]=j; } } } } //a[i]表示以i和i+1间的缝隙为中心的最长回文串的长度的一半 struct BIT{ int c[N]; void init(){for(int i=1;i<=2*n;i++)c[i]=0;} void add(int x,int v){for(;x<=2*n;x+=x&-x)c[x]+=v;} int ask(int x){int res=0;for(;x;x-=x&-x)res+=c[x];return res;} }B; struct query{int l,r,id;}f[N]; int id[N],cnt; bool cmp1(int i,int j){return i-a[i]<j-a[j];} bool cst1(query a,query b){return a.l<b.l;} bool cst2(query a,query b){return rk[a.l]>rk[b.l];} void init(){ memset(a,0,sizeof(a)); memset(sa,0,sizeof(sa)); memset(ans,0,sizeof(ans)); memset(c,0,sizeof(c)); memset(x,0,sizeof(x)); memset(y,0,sizeof(y)); } int main(){ // freopen("string.in","r",stdin); // freopen("string.out","w",stdout); scanf("%d%d",&test,&T); while(T--){ init(); scanf("%d%d",&n,&q); scanf("%s",s+1); for(int i=1;i<=n;i++)t[i]=t[2*n+1-i]=s[i]; Manacher();SA(); for(int i=1;i<=q;i++) scanf("%d%d",&f[i].l,&f[i].r),f[i].id=i; B.init();cnt=0; for(int i=1;i<n;i++) if(a[i]&&rk[i+1-a[i]]<rk[2*n+1-i-a[i]])id[++cnt]=i; sort(id+1,id+cnt+1,cmp1);sort(f+1,f+q+1,cst1); for(int i=1,p=1;i<=q;i++){ while(p<=cnt&&id[p]-a[id[p]]+1<=f[i].l) {B.add(id[p],1);++p;} ans[f[i].id]-=B.ask(f[i].l+f[i].r-1)-B.ask(f[i].l-1); } B.init();cnt=0; for(int i=2*n;i>=1;i--) if(sa[i]&1)id[++cnt]=i; sort(f+1,f+q+1,cst2); for(int i=1,p=1;i<=q;i++)if(f[i].l&1){ while(p<=cnt&&id[p]>rk[f[i].l]){B.add(sa[id[p]],1);++p;} ans[f[i].id]+=B.ask(2*n-f[i].l)-B.ask(2*n+1-2*f[i].r-f[i].l); } B.init();cnt=0; for(int i=2*n;i>=1;i--) if(!(sa[i]&1))id[++cnt]=i; for(int i=1,p=1;i<=q;i++)if(!(f[i].l&1)){ while(p<=cnt&&id[p]>rk[f[i].l]){B.add(sa[id[p]],1);++p;} ans[f[i].id]+=B.ask(2*n-f[i].l)-B.ask(2*n+1-2*f[i].r-f[i].l); } for(int i=1;i<=q;i++)printf("%d\n",ans[i]); } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具