[题目记录]P9999 [Ynoi2000] tmostnrq

news/2025/1/12 16:59:26/文章来源:https://www.cnblogs.com/youlv/p/18667075

P9999 [Ynoi2000] tmostnrq

题意

给定 \(n\) 个顶点的树,顶点编号为 \(1,\dots,n\),给定长度 \(n_0\) 的序列 \(a_1,\dots,a_{n_0}\),共 \(m\) 次查询,每次查询给定 \(l,r,x\),问树的顶点 \(x\),依次向 \(a_l,\dots,a_r\) 移动一步,到达的顶点。

\(x=y\),则从顶点 \(x\)\(y\) 移动一步到达 \(x\),否则到达与 \(x\) 在树上相邻且距离 \(y\) 最近的位置。

$ n,m\le 1e6 $ , 时限 \(12 \mathrm s\) .


题解

发现移动过程不能简单地维护 , 考虑把一次移动看成一个函数 , 这就变成了一个函数复合问题 , 考虑离线用 lxl 讲的插入-标记-回收解决 .

考虑如何处理移动的过程 , 发现向 \(x\) 移动一位相当于把从 \(x\) 到根的路径全沿这条路径下移 , 其他点都上移 , 大概如图 :

处理链的问题 , 就使用树链剖分 , 每次把向下跳的这条链单独处理 , 其余的向上跳打全局标记 .

对于每一条链 , 用以深度为关键字的 FHQ treap 维护 , 每次插入一个点 , 就直接把它插入到对应的链上 .

问题的关键是每一次移动的维护 :

每次处理链时 , 设向这条链上\(u\) 移动 , 相当于把平衡树分为 \(<dep_u\)\(>dep_u\) 两部分 , 左边深度 \(+1\) , 右边深度 \(-1\) , 再合并起来 . 这样操作的总复杂度是 \(O(n\log^2 n)\) .

但是发现跳的过程中会出现一个点跳到了不同链上的情况 :

  • 第一种 : 设向这条链上\(u\) 移动 , 但 \(u\) 不是最初的起点 \(a_i\) , 这时 \(u\) 要通过向 \(a_i\) 方向移动的虚边跳到后一个链上 .

    因此如果 \(u\) 处有点 , 就把直接它插入到上一条链中 .

  • 第二种 : 如图中蓝边 , 打全局上移标记时 , 原来就在链顶的点会通过虚边上移 .

    因此每次操作后需要找到所有出现这种问题的链 , 把它的链顶点删掉插入到它的父亲所在链中 .

处理第二种的方法看起来十分暴力 , 但是稍加分析 , 每个点只有在通过虚边切换时会有额外复杂度 , 如果不考虑下移 , 每个点的切换次数是 $O(\log n) $ 次的 .

而考虑下移 , 每次操作也最多把 $ O(\log n) $ 个点通过虚边下移 , 相当于每次操作使后面多了 \(O(\log n)\) 次上移 . 也就是说 , 视每个点还能向上到根的次数为它的势能 , 总势能是 $O(n\log n+m\log n) $ 的 , 每消除一势能的复杂度是 $O(\log n) $ 的 , 所以这部分复杂度也是 \(O(n\log^2 n)\) , 最终复杂度就是 $O(n\log ^2 n) $ .

为了保证均摊部分的复杂度以及打整体标签的正确性 , 操作的实现都要足够精细 :

  • 如果两个点的位置重合 , 需要用并查集把他们并在一起 , 始终保证同一位置最多只有一个点 .

    否则 , 就无法保证跨虚边下移时 , 每一条虚边只有一个点下移 , 也就无法保证复杂度 .

  • 每次整体打上移标签 , 相当于让深度整体 \(-1\) , 但是我们手动调整的链部分是不能受它影响的 , 所以除了整体标记 \(tottag\) 要对每条链单独维护一个标记 \(tag_i\) . 设平衡树里记录的深度是 \(val\) , 它的深度真实值就应该是 \(val+tottag+tag_i\) .

    这样每次打标签的操作就是 \(tottag--\) , 然后所有从 \(x\)\(1\) 经过的链 \(tag_i++\) .

    此外 , 为了方便操作 , 可以在对平衡树操作前更新 \(val\gets val+tottag+tag_i , tag_i\gets tottag\) .

  • 每次处理上移操作不能遍历所有链判断链顶是否有点 , 正确的做法是维护链中深度最小的点到顶的距离 . 设平衡树上深度最小值为 \(mn\) , 根据我们刚才的定义 , 它到顶的距离是 \(mn+tottag+tag_i-dep_{top}\) , 需要操作的链 , 满足 :

    \[mn+tottag+tag_i<dep_{top} \\ mn+tag_i-dep_{top}<-tottag \]

    可以用 set 维护所有链的 \(mn+tag_i-dep_{top}\) , 每次对平衡树操作时在 set 中更新对应的值 . 处理上移操作时直接取出 \(<-tottag\) 的链进行操作即可 .

  • 注意打标签的时机 , 操作时相当于先手动操作了所有在路径上的链 , 然后对它们 \(tag_i++\) 用来抵消整体 \(tottag\) , 不要一边操作一边动 \(tag\) , 否则可能导致从后面的链下移过来的点位置出错 .

  • 查询时可以直接记录对应的平衡树节点编号 ( 注意要取并查集的根 ) , 注意要把这个节点在平衡树上的祖先链都 pushdown , 还要考虑链上的 \(tag\) , 这样就可以取出它的深度 , 结合所在的重链就可以取出具体位置 .

点击查看代码
#include<bits/stdc++.h>
#define file(x) freopen(#x ".in","r",stdin),freopen(#x ".out","w",stdout)
#define ll long long
using namespace std;constexpr int N=1e6+5;
constexpr int INF=1e7;struct node{int ls,rs,rnd,fa,val,tag;
}a[N];
int at;inline int add(int dep){a[++at]={0,0,rand(),0,dep,0};return at;
}inline void pushdown(int x){a[a[x].ls].val+=a[x].tag;a[a[x].ls].tag+=a[x].tag;a[a[x].rs].val+=a[x].tag;a[a[x].rs].tag+=a[x].tag;a[x].tag=0;
}inline void pushup(int x){if(a[x].ls) a[a[x].ls].fa=x;if(a[x].rs) a[a[x].rs].fa=x;a[x].fa=0;
}void split(int u,int k,int &x,int &y){if(!u) {x=y=0;return;}pushdown(u);if(a[u].val<=k){x=u;split(a[u].rs,k,a[x].rs,y);pushup(x);}else{y=u;split(a[u].ls,k,x,a[y].ls);pushup(y);}
}int merge(int x,int y){if(x==0||y==0) return x+y;pushdown(x);pushdown(y);if(a[x].rnd<=a[y].rnd){a[x].rs=merge(a[x].rs,y);pushup(x);return x;}else{a[y].ls=merge(x,a[y].ls);pushup(y);return y;}return 0;
}int get(int x,int k){if(!x) return 0;pushdown(x);if(a[x].val>k) return get(a[x].ls,k);if(a[x].val<k) return get(a[x].rs,k);return x;
}int getmin(int x){if(!x) return INF;pushdown(x);if(a[x].ls) return getmin(a[x].ls);return a[x].val;
}int fd[N];
int find(int x){if(x==fd[x]) return x;fd[x]=find(fd[x]);return fd[x];
}int n,tn,m,to[N];vector<int> e[N];vector<pair<int,int> > inp[N];
vector<int> oup[N];
int nump[N],res[N];int fa[N],dep[N],son[N],sz[N],top[N],dfn[N],rnk[N],L[N],R[N],rt[N],tot,pos[N];int ft[N][24];void dfs1(int u){ft[u][0]=fa[u];for(int i=1;i<=20;i++)ft[u][i]=ft[ft[u][i-1]][i-1];sz[u]=1;for(auto v:e[u]){if(v==fa[u]) continue;fa[v]=u;dep[v]=dep[u]+1;dfs1(v);sz[u]+=sz[v];if(sz[v]>sz[son[u]]) son[u]=v;}
}int tottag,tag[N],mn[N];
set<pair<int,int> > st;void dfs2(int u,int t){dfn[u]=++tot;rnk[tot]=u;top[u]=t;if(u==t){L[t]=u;mn[t]=INF;tag[t]=0;st.insert({INF-dep[t],t});}R[t]=u;if(son[u])dfs2(son[u],t);for(auto v:e[u])if(v!=fa[u]&&v!=son[u])dfs2(v,v);
}inline void pushtag(int t){int tg=tottag+tag[t];a[rt[t]].val+=tg,a[rt[t]].tag+=tg;mn[t]+=tg;tag[t]=-tottag;
}inline void insert(int x,int t){if(!x) return;int d=a[x].val;pushtag(t);int link=get(rt[t],d);if(link) fd[x]=link;else{st.erase({mn[t]+tag[t]-dep[t],t});int u,v;split(rt[t],d,u,v);rt[t]=merge(u,merge(x,v));pos[rt[t]]=t;mn[t]=getmin(rt[t]);st.insert({mn[t]+tag[t]-dep[t],t});}
}inline void solve(int x){int u,v,w,y,z,tmp,t;pushtag(top[x]);split(rt[top[x]],dep[x]-2,u,tmp);split(tmp,dep[x]+1,v,z);tmp=v;split(tmp,dep[x]-1,v,y);tmp=y;split(tmp,dep[x],w,y);a[v].val++;a[v].tag++;a[y].val--;a[y].tag--;if(v&&w) fd[v]=w,v=0;if(y&&w) fd[y]=w,y=0;if(v&&y) fd[v]=y,v=0;a[u].val++;a[u].tag++;a[z].val--;a[z].tag--;rt[top[x]]=merge(u,merge(v+w+y,z));pos[rt[top[x]]]=top[x];int last=x;while(top[x]!=1){last=x;x=fa[top[x]];pushtag(top[x]);split(rt[top[x]],dep[x]-2,u,tmp);split(tmp,dep[x]+1,v,z);tmp=v;split(tmp,dep[x]-1,v,y);tmp=y;split(tmp,dep[x],w,y);t=top[last];st.erase({mn[t]+tag[t]-dep[t],t});if(w){a[w].val++,a[w].tag++;int d=a[w].val;int link=get(rt[t],d);if(link) fd[w]=link;else rt[t]=merge(w,rt[t]);pos[rt[t]]=t;}tag[t]++;mn[t]=getmin(rt[t]);st.insert({mn[t]+tag[t]-dep[t],t});a[v].val++;a[v].tag++;a[y].val--;a[y].tag--;if(v&&y) fd[v]=y,v=0;a[u].val++;a[u].tag++;a[z].val--;a[z].tag--;rt[top[x]]=merge(u,merge(v+y,z));pos[rt[top[x]]]=top[x];}t=top[x];st.erase({mn[t]+tag[t]-dep[t],t});tag[t]++;mn[t]=getmin(rt[t]);st.insert({mn[t]+tag[t]-dep[t],t});tottag--;vector<int> d;d.clear();for(auto x:st){if(x.first>=-tottag) break;d.push_back(x.second);}for(auto t:d){pushtag(t);st.erase({mn[t]+tag[t]-dep[t],t});split(rt[t],dep[t]-1,u,v);rt[t]=v;pos[v]=t;mn[t]=getmin(rt[t]);st.insert({mn[t]+tag[t]-dep[t],t});insert(u,top[fa[t]]);}
}inline int query_pos(int u,int d){for(int i=20;i>=0;i--)if(dep[ft[u][i]]>=d) u=ft[u][i];return u;
}int q[N],tp;inline int query(int x){tp=0;q[++tp]=x;while(a[x].fa) q[++tp]=a[x].fa,x=a[x].fa;int st=R[pos[q[tp]]];pushtag(pos[q[tp]]);while(tp) pushdown(q[tp--]);return query_pos(st,a[q[1]].val);
}namespace debug{int s;int tres[N],fa[N];void dfs(int u){for(auto v:e[u]){if(v==fa[u]) continue;fa[v]=u; dfs(v);}}int solve(int l,int r,int x){for(int i=l;i<=r;i++){fa[to[i]]=to[i];dfs(to[i]);x=fa[x];}return x;}void init(){cin>>s;srand(s);n=15;tn=10;m=10;cout<<n<<' '<<tn<<' '<<m<<'\n';for(int i=2;i<=n;i++){int tmpf=rand()%(i-1)+1;cout<<tmpf<<' ';e[tmpf].push_back(i);e[i].push_back(tmpf);}cout<<'\n';for(int i=1;i<=tn;i++){to[i]=rand()%n+1;cout<<to[i]<<' ';}cout<<'\n';for(int i=1;i<=m;i++) {int l=rand()%tn+1,r=rand()%tn+1,x=rand()%n+1;if(l>r) swap(l,r);cout<<l<<' '<<r<<' '<<x<<'\n';inp[l].push_back({i,x});oup[r].push_back(i);tres[i]=solve(l,r,x);}}void check(){for(int i=1;i<=m;i++){if(res[i]!=tres[i]){cout<<"WA! pos:"<<i<<" answer="<<tres[i];return;}}cout<<"AC!";}
}void input(){cin>>n>>tn>>m;for(int i=2;i<=n;i++) {int u,v;cin>>u;v=i;e[u].push_back(v);e[v].push_back(u);}for(int i=1;i<=tn;i++) cin>>to[i];for(int i=1;i<=m;i++){int l,r,x;cin>>l>>r>>x;inp[l].push_back({i,x});oup[r].push_back(i);}
}int main(){ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);//debug::init();input();dep[1]=1;dfs1(1);dfs2(1,1);for(int i=1;i<=m;i++) fd[i]=i;for(int i=1;i<=tn;i++){for(auto [id,u]:inp[i]){int now=add(dep[u]);insert(now,top[u]);nump[id]=now;}solve(to[i]);for(auto id:oup[i]){res[id]=query(find(nump[id]));}}for(int i=1;i<=m;i++) cout<<res[i]<<'\n';//debug::check();}

总结

结合 lxl 讲的这一类问题的套路 , 这道题的思路其实还是很清晰的 . 甚至看起来细节都不是特别繁琐 .

然而实现起来还是非常难写 , 因为维护的是深度 , 还有全局打 tag , 合并相同点的问题 , 导致所有操作都没有看起来那么直接 .

因为以下问题调了 14h+ :

  • 打标签时机出错 .

  • 并查集预处理要 \(m\) 个而非 \(n\) 个.

  • pushup(y) 写成 pushup(x) .

  • 合并点少考虑情况 .

  • 查询时没有考虑全局 tag

其中最后一个问题卡了一个上午加一个下午 . 手搓数据全都没看出问题 , 最后是靠对拍拍出了问题 . 所以在真的找不出问题时还是对拍比较有效 .

不管怎么说终于调出来这道题了 .

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/868252.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

多光谱火焰智能监测摄像机

多光谱火焰智能监测摄像机将在未来继续发展。未来的摄像机可能会更加智能化,具备更强的自主识别和决策能力,进一步提升火灾监测的灵敏度和准确性。此外,随着物联网的发展,这些摄像机还有望与其他设备和系统实现更深入的数据共享和集成应用,为整个安全监控体系增添新的可能…

明火智能监测摄像机

明火智能监测摄像机检测到火源时,它会立即触发报警机制,以警告人员采取适当的行动。它可以与各种报警系统集成,发出清晰可闻的警报声,并通过网络将警报信息发送到相关部门或人员的手机应用程序,确保快速响应。明火智能监测摄像机全天候工作,不间断地监测环境中的火情。它…

山体滑坡自动监测识别摄像机

山体滑坡自动监测识别摄像机系统通过技术创新和智能化应用,为减少自然灾害带来的损失和风险提供了重要的技术手段和支持。随着技术的进步和系统的完善,相信这类系统将在全球范围内得到广泛应用,为社会公共安全和环境保护作出更大贡献。对于现代社会而言,山体滑坡作为自然灾…

水位智能监测识别摄像机

水位智能监测识别摄像机的应用前景十分广阔。未来,这些摄像机有望进一步提升监测精度和反应速度,扩展到更多复杂的水域环境和特殊应用场景中,为保障社会公共安全和生态环境做出更大贡献。总之,水位智能监测识别摄像机以其先进的技术和多功能性能,正在成为水域监测领域的重…

1.12 CW 模拟赛 T1. 括号序列

思路 根据赛时的检验, 典型的动点问题的 \(\rm{trick}\) 并不能在这里使用, 也就是说, 分类讨论前缀 + \(i\) + 后缀 前缀 + \(i\) 后缀 + \(i\)是不可行的 考虑括号串问题的常见做法, 先将其赋值成 \(1, -1\) 之后进行处理 你发现这种做法有枚举字段和的瓶颈, 所以也不可行 当…

【Docker】Docker搭建一款开源的RAG聊天应用

项目介绍 Kotaemon是一个由Cinnamon开发的开源项目,旨在提供一个基于检索增强生成(RAG)技术的用户界面,使用户能够与自己的文档进行交互问答。 功能特点 文档问答:Kotaemon允许用户通过自然语言查询与文档进行交互,快速获取所需信息。这一功能在学术研究、企业文档管理等…

客梯自动监测识别摄像机

客梯自动监测识别摄像机通过技术创新和智能化应用,为现代城市客梯的安全管理提供了全新的解决方案。随着技术的不断进步和应用场景的扩展,相信这类系统将在全球范围内得到广泛应用,为乘客的安全出行和客梯运行的高效管理提供更加可靠的保障和支持。当今社会,随着城市建设的…

【Docker】Docker搭建一款开源的加密货币量化交易平台

项目介绍 Freqtrade是一个开源的加密货币量化交易平台,它允许用户通过编写和配置交易策略来自动化交易过程。 功能特点 开源性:Freqtrade的代码是开源的,这意味着用户可以查看、修改和扩展平台的功能。 自动化交易:通过配置交易策略,Freqtrade可以自动执行买卖操作,无需人…

高级数据结构与算法---莫队

这篇文章主要是用来复习的,最近学了一些新的东西,多少要记录一下,不然以后忘了,不过似乎树状数组和ST表还没有补完,等后面有时间(不能拖拉)再去将他们给写完,然后就开始去学习一下计算几何,树形DP以及图论,啊啊啊啊啊啊,还要准备数学建模,哎,为什么明明都放假了,…

AT_abc388_f Dangerous Sugoroku 题解

太幽默了。 显然可以用矩阵快速幂解决,矩阵里维护距离当前点 \(B\) 以内的所有点可不可达,转移只需分段,在区间内和不在区间内用不同的转移矩阵即可。复杂度 \(O(B^3m\log n)\)。 然后你就 T 了。 此时你很急,你现在应该快点卡常来 AK 这场比赛而不是研究其他的做法,于是我…

多路复用与多路分解

主机维护一个Socket表,通过表中记录的ipport与socket对应关系进行封装报文,接受到报文时使用报文的ip和port查找对应socket的过程 称为多路复用与多路分解 TCP socket存有本机进程到对端进程的映射关系UDPudp是无连接的,本机ip port没有和对端ip port构成映射关系,udp的soc…

计算机网络-多路复用与多路分解

主机维护一个Socket表,通过表中记录的ipport与socket对应关系进行封装报文,接受到报文时使用报文的ip和port查找对应socket的过程 称为多路复用与多路分解 TCP socket存有本机进程到对端进程的映射关系UDPudp是无连接的,本机ip port没有和对端ip port构成映射关系,udp的soc…