题解 P5819 【【L&K R-03】音游大计算】

· · 题解

\mathsf{Preface}

题面传送门

题意简述:题面太长,细节太多,没法简述。。。

看起来现在的 \sf{AC} 记录里面除了我和出题人,别的都是抄题解的。。(截至 2020.2.21\ 23:08

锻炼码力 + \rm{debug} 能力的题目。我对拍了无数次才把自己的错误一一找出来。

吐槽一下:这!题!的!细!节!是!真!T!M!多!

为了我花在这道题目上的 \rm{3.5h},我一定得写篇题解。

\sf{Solution} \sf{Step\ One:Prework}

敢做这道题目的应该都能一眼看出是线段树。。。

我们先把所有位置离散化,然后将 \rm{key} 按下落时间排序,将 \rm{click} 按点击时间排序,方便统计答案。

该部分的注意点:

```cpp const int N=1.2e5+5,inf=2e9+7; struct key{ int t,a,b,sta,ju;//sta是这个key的状态,ju是这个key被判定的时间 bool operator < (const key &v) const {return t<v.t;}//按时间排序 }c[N]; struct click{ int t,pos; bool operator < (const click &v) const {return t<v.t;} }q[N]; int n,m,cnt,pos[N<<2]; ......main函数内 read(n),read(m); for(int i=1;i<=n;i++){ double x,y,z; read(x),read(y),read(z); c[i]={x*1e5+0.5,y*1e5+0.5,z*1e5+0.5,0}; pos[++cnt]=c[i].a,pos[++cnt]=c[i].b; } for(int i=1;i<=m;i++){ double x,y; read(x),read(y); q[i]={x*1e5+0.5,y*1e5+0.5}; pos[++cnt]=q[i].pos; } sort(c+1,c+n+1),sort(q+1,q+m+1),c[0].t=inf;//c[0].t赋为inf,更方便 sort(pos+1,pos+cnt+1); cnt=unique(pos+1,pos+cnt+1)-pos-1; ``` $$\sf{Step\ Two:Build\ SegmentTree}$$ 将相应的 $\rm{key}$ 添加到线段树相应的位置上,因为线段树的一个区间可能会被多个 $\rm{key}$ 覆盖,所以每个区间可以存一个链表,链表里面存 $\rm{key}$ 的编号。 - 因为位置离散化后会达到 $2n+m$,所以空间要开大一些。(反正不会 $\sf{MLE}$ 就行) 线段树的建造: ```cpp int get(int x){return lower_bound(pos+1,pos+cnt+1,x)-pos;}//返回离散化后的值 int node,hd[N<<4],tl[N<<4],nxt[N<<7],val[N<<7]; void push(int x,int id){//链表添加 if(!hd[x])hd[x]=tl[x]=++node; else nxt[tl[x]]=++node,tl[x]=node; val[node]=id; } void add(int l,int r,int ql,int qr,int x,int id){//添加key if(ql<=l&&r<=qr){ push(x,id); return; } int m=l+r>>1; if(ql<=m)add(l,m,ql,qr,x<<1,id); if(m<qr)add(m+1,r,ql,qr,x<<1|1,id); } ......main函数内 for(int i=1;i<=n;i++){ int p1=get(c[i].a),p2=get(c[i].b); add(1,cnt,p1,p2,1,i); } ``` $$\sf{Step\ Three:Query}$$ 对每一个点击,先求出它产生判定效果的 $\rm{key}$ 是在什么时候下落的,如果此次点击时间为 $T$,位置为 $P$,也就是求出 $\min_{a_i\leq P\leq b_i,\ T-0.6s<t_i<T+1s}t_i$,记为 $t$。(区分 $t$ 和 $T$) 另外,在求 $t$ 的时候要将前面已经 “过期” 或者 “已被判定” 的 $\rm{key}$ 弹出。 给出此部分的代码: ```cpp #define d val[hd[x]]//宏定义方便 void update(int x,int t){//将过期或已被判定的key弹出 while(hd[x]){ if(c[d].t+60000<=t){//过期 if(!c[d].sta)c[d].ju=c[d].t+60000,c[d].sta=1; hd[x]=nxt[hd[x]]; } else if(c[d].sta)hd[x]=nxt[hd[x]];//已被判定 else break;//否则break } } int tim(int x,int t){//求一下会判定的key的下落时间 if(c[d].t-t>=100000)return inf;//如果还没到判定范围就返回inf(如果链表为空,因为前面c[0].t赋为inf了,所以也会返回inf) else return c[d].t; } int query(int l,int r,int pos,int x,int t){ update(x,t); if(l==r)return tim(x,t);//如果到底了就返回 int m=l+r>>1; if(pos<=m)return min(tim(x,t),query(l,m,pos,x<<1,t)); else return min(tim(x,t),query(m+1,r,pos,x<<1|1,t));//因为判定的是最低的key,所以取min } ``` 接下来判定所有下落时间为 $t$ 的 $\rm{key}$。 注意点:如果没有可以判定的 $\rm{key}$(即代码中 $t=\rm{inf}$),跳过此部分,否则可能会导致死循环。 ```cpp void modify(int l,int r,int pos,int x,int t,int T){ while(c[d].t==t){//如果下落时间为本次key的判定下落时间t if(!c[d].sta){//如果没有被判定 c[d].ju=T;//记录被判定的时间T(再次说明,请区分t和T) if(abs(c[d].t-T)<20000)c[d].sta=3;//根据题目所给的条件进行判定,下同 else if(abs(c[d].t-T)<60000)c[d].sta=2; else c[d].sta=1; } hd[x]=nxt[hd[x]]; } if(l==r)return; int m=l+r>>1; if(pos<=m)modify(l,m,pos,x<<1,t,T); else modify(m+1,r,pos,x<<1|1,t,T); } ......main函数内 for(int i=1;i<=m;i++){ int p=get(q[i].pos),t=query(1,cnt,p,1,q[i].t); if(t!=inf)modify(1,cnt,p,1,t,q[i].t);//如果有判定的key就判定 } ``` $$\sf{Step\ Four:Calculate\ Answer}$$ 有了每个 $\rm{key}$ 的判定时间和判定状态,答案就很好计算了,将每个 $\rm{key}$ 按照判定时间排序,判定时间相同按 $\sf{miss,good,perfect}$ 排序即可。 此部分代码: ```cpp bool cmp(key a,key b){return a.ju<b.ju||a.ju==b.ju&&a.sta<b.sta;} int mis,goo,per,com,tmp; ......main函数内 for(int i=1;i<=n;i++)if(!c[i].sta)c[i].sta=1,c[i].ju=c[i].t+60000;//如果未被判定,那就是miss,判定时间为下落时间+0.6s sort(c+1,c+n+1,cmp);//根据判定时间排序 for(int i=1;i<=n;i++){ if(c[i].sta==1)com=max(com,tmp),mis++,tmp=0; else tmp++,c[i].sta==2?goo++:per++;//计算答案 } cout<<per<<" "<<goo<<" "<<mis<<" "<<max(tmp,com)<<endl; ``` 至此,本题被我们完美解决! --- $$\sf{Code}$$ 给出完整的代码: ```cpp #include<bits/stdc++.h> using namespace std; char buf[1<<23],*p1=buf,*p2=buf; #define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++) inline void read(int &x){ bool sign=0; char s=getchar(); x=0; while(!isdigit(s))sign|=s=='-',s=getchar(); while(isdigit(s))x=(x<<1)+(x<<3)+s-'0',s=getchar(); x=sign?-x:x; } inline void read(double &x){ bool sign=0; char s=getchar(); int a=0; x=0; while(!isdigit(s))sign|=s=='-',s=getchar(); while(isdigit(s))a=(a<<1)+(a<<3)+s-'0',s=getchar(); if(s=='.'){ double tmp=1; s=getchar(); while(isdigit(s))tmp/=10,x+=tmp*(s-'0'),s=getchar(); } x+=a,x=sign?-x:x; } #define d val[hd[x]]//宏定义方便 const int N=1.2e5+5,inf=2e9+7; struct key{ int t,a,b,sta,ju;//sta是这个key的状态,ju是这个key被判定的时间 bool operator < (const key &v) const {return t<v.t;}//按时间排序 }c[N]; struct click{ int t,pos; bool operator < (const click &v) const {return t<v.t;} }q[N]; bool cmp(key a,key b){return a.ju<b.ju||a.ju==b.ju&&a.sta<b.sta;} int n,m,cnt,pos[N<<2]; int get(int x){return lower_bound(pos+1,pos+cnt+1,x)-pos;}//返回离散化后的值 int node,hd[N<<4],tl[N<<4],nxt[N<<7],val[N<<7]; void push(int x,int id){//链表添加 if(!hd[x])hd[x]=tl[x]=++node; else nxt[tl[x]]=++node,tl[x]=node; val[node]=id; } void add(int l,int r,int ql,int qr,int x,int id){//添加key if(ql<=l&&r<=qr){ push(x,id); return; } int m=l+r>>1; if(ql<=m)add(l,m,ql,qr,x<<1,id); if(m<qr)add(m+1,r,ql,qr,x<<1|1,id); } void update(int x,int t){//将过期或已被判定的key弹出 while(hd[x]){ if(c[d].t+60000<=t){//过期 if(!c[d].sta)c[d].ju=c[d].t+60000,c[d].sta=1; hd[x]=nxt[hd[x]]; } else if(c[d].sta)hd[x]=nxt[hd[x]];//已被判定 else break;//否则break } } int tim(int x,int t){//求一下会判定的key的下落时间 if(c[d].t-t>=100000)return inf;//如果还没到判定范围就返回inf(如果链表为空,因为前面c[0].t赋为inf了,所以也会返回inf) else return c[d].t; } int query(int l,int r,int pos,int x,int t){ update(x,t); if(l==r)return tim(x,t);//如果到底了就返回 int m=l+r>>1; if(pos<=m)return min(tim(x,t),query(l,m,pos,x<<1,t)); else return min(tim(x,t),query(m+1,r,pos,x<<1|1,t));//因为判定的是最低的key,所以取min } void modify(int l,int r,int pos,int x,int t,int T){ while(c[d].t==t){//如果下落时间为本次key的判定下落时间t if(!c[d].sta){//如果没有被判定 c[d].ju=T;//记录被判定的时间T(再次说明,请区分t和T) c[d].sta=abs(c[d].t-T)<20000?3:abs(c[d].t-T)<60000?2:1;//根据题目所给的条件进行判定 } hd[x]=nxt[hd[x]]; } if(l==r)return; int m=l+r>>1; if(pos<=m)modify(l,m,pos,x<<1,t,T); else modify(m+1,r,pos,x<<1|1,t,T); } int mis,goo,per,com,tmp; int main(){ read(n),read(m); for(int i=1;i<=n;i++){ double x,y,z; read(x),read(y),read(z); c[i]={x*1e5+0.5,y*1e5+0.5,z*1e5+0.5,0}; pos[++cnt]=c[i].a,pos[++cnt]=c[i].b; } for(int i=1;i<=m;i++){ double x,y; read(x),read(y); q[i]={x*1e5+0.5,y*1e5+0.5}; pos[++cnt]=q[i].pos; } sort(c+1,c+n+1),sort(q+1,q+m+1),c[0].t=inf;//c[0].t赋为inf,更方便 sort(pos+1,pos+cnt+1),cnt=unique(pos+1,pos+cnt+1)-pos-1;//离散化 for(int i=1;i<=n;i++)add(1,cnt,get(c[i].a),get(c[i].b),1,i); for(int i=1;i<=m;i++){ int p=get(q[i].pos),t=query(1,cnt,p,1,q[i].t); if(t!=inf)modify(1,cnt,p,1,t,q[i].t);//如果有判定的key就判定 } for(int i=1;i<=n;i++)if(!c[i].sta)c[i].sta=1,c[i].ju=c[i].t+60000;//如果未被判定,那就是miss,判定时间为下落时间+0.6s sort(c+1,c+n+1,cmp);//根据判定时间排序 for(int i=1;i<=n;i++){ if(c[i].sta==1)com=max(com,tmp),mis++,tmp=0; else tmp++,c[i].sta==2?goo++:per++;//计算答案 } cout<<per<<" "<<goo<<" "<<mis<<" "<<max(tmp,com)<<endl; return 0; } ``` 开了 $\sf{O2}$ 跑得飞快,不开 $\sf{O2}$ 可能会因为评测机波动 $\sf{TLE}$(只是可能,因为还没背卡过,最长的测试点在 $\sf{900ms}$ 左右) --- $$\sf{Ending}$$ 如果你只对 $\rm{key}$ 的位置离散化,其实会快不少,但细节更多,有一些边界情况需要考虑。如果你想追求更快的解法,可以参考我的这份评测记录:[$\sf{link}$](https://www.luogu.com.cn/record/30906960)(快了大概 $25\%$ 左右) 其实当时打这场比赛的时候就想写正解,但是细节太多写自闭了。。。 今天无意间翻到这道题,仍然用了很长时间才将其 $\sf{AC}$,可见我是多么的菜。 码字不易,既然你都看到这儿了,就顺手点个赞呗 =w= 当然,如果你发现了本题解的笔误/学术上的错误,欢迎在评论区指出或者直接私信通知我,万分感谢! 再次感谢你认真地翻到了这里,拜拜 ~~