Soratosorato

CF1254D Tree Queries 题解

Sorato·2024-10-15 19:44·15 次阅读

CF1254D Tree Queries 题解

CF1254D Tree Queries 题解

题目大意#

给定一棵 N 个节点的树,有 Q 次操作,分别是如下两种:

  • 给定一个点 v 和一个权值 d,等概率地选择一个点 r,对每一个点 u,若 vur 的路径上,则u的权值加上d
  • 查询 v 的权值期望,对 998244353 取模。

Solve#

考虑对节点 u 执行操作 1 之后,对树上所有节点的贡献。

  • 对于 u 自己,期望权值需要加上 d
  • 对于 u 子树里(除去 u 自己)的点,若这个点在以 v 为根的子树里(vson(u)),那么只有选到的 r 不在 v 的子树里才对这个点有贡献,所以这个点的期望权值需要加上 dsiz(v)N
  • 对于不在 u 子树里的点,需要选到的 ru 子树里,才能对这个点有贡献,期望权值需要加上 dsiz(u)N

对树建立 dfs 序,那么就可以转化为区间加,用树状数组维护。

但问题是,对 u 子树里的点的贡献和那个点所在的子树有关,如果每次都遍历 u 的所有儿子,可以被菊花卡到 O(NQlog2N)。所以考虑一个根号分治 / 根号重建。

将操作分为若干个大小为 B 的块。记录每个块内的修改操作,把对同一个节点的修改合并。

B 次修改,遍历这个块内的所有修改并按上面的方式执行。由于每个点的所有边至多被遍历一次,复杂度 O(QBNlog2N)

对于一次询问,它所在块前面的贡献都已经通过数据结构统计出来了,只需要遍历这个块里的所有修改就行。

比如当前询问的点是 u,遍历到的修改是 (v,d)。还是按上面所说的分类讨论计算贡献。那么我们需要快速求出 u 是否在 v 的子树里,若在,是在 v 的哪个儿子的子树里。可以分情况讨论。

  • u=v,特判。
  • 否则若 u 的深度 dep(u)dep(v),说明 u 肯定不在 v 的子树里。
  • dep(u)<dep(v),我们对 udep(v)dep(u)1 级祖先,记为 fa。如果 fa 的父亲不是 v,说明 u 不在 v 的子树里。否则,uv 的子树里,并且我们也已求得了它是在 v 的哪个儿子的子树里。

这样,就可以计算贡献了。时间复杂度为 O(QBK),其中 K 为求 k 级祖先的复杂度消耗。

对于求 k 级祖先,方法很多。由于我们对于不同块的操作已经带了一个 log,如果我们在这里还使用带 log 的算法求,如树剖和倍增,那么总复杂度是在 B=N 时取得最小值,为 O(QNlog2N),较慢。实测倍增写法跑了 4000ms+。

用长链剖分求 k 级祖先,可以做到 O(Nlog2N)O(1)。这样总复杂度在 B=Nlog2N 时取得最小值 O(QNlog2N)

Code#

Copy
#include<bits/stdc++.h> using namespace std; typedef long long ll; inline int read() { short f=1; int x=0; char c=getchar(); while(c<'0'||c>'9') {if(c=='-') f=-1;c=getchar();} while(c>='0'&&c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar(); return x*f; } const int N=1.5e5+10,B=1600,MOD=998244353; inline int qpow(ll x,int y) { ll res=1; while(y) { if(y&1) res=res*x%MOD; x=x*x%MOD;y>>=1; } return res; } int n,q,dfn[N],tim,inv,siz[N]; ll d[N]; int dep[N],fa[N][18],logn[N]={-1}; bool in[N]; vector<int>e[N],v; int son[N],mx[N],l[N],r[N],top[N]; void get_son(int u)//预处理重儿子,倍增预处理 2^k 级祖先 { mx[u]=dep[u]=-~dep[fa[u][0]]; for(int i=1;i<=logn[dep[u]];i=-~i) fa[u][i]=fa[fa[u][~-i]][~-i]; for(int i:e[u]) if(i!=fa[u][0]) fa[i][0]=u,get_son(i),siz[u]+=siz[i]; if(mx[u]>mx[son[fa[u][0]]]) son[fa[u][0]]=u,mx[fa[u][0]]=mx[u]; } void get_chain(int u,int L)//重儿子优先遍历 { r[dfn[u]=tim=-~tim]=u; l[tim]=L; top[u]=fa[u][0]&&u==son[fa[u][0]]?top[fa[u][0]]:u; if(son[u]) get_chain(son[u],fa[L][0]); for(int i:e[u]) if(i!=son[u]&&i!=fa[u][0]) get_chain(i,i); } inline int ask(int u,int k)//长剖求 k 级祖先 { if(!k) return u; u=fa[u][logn[k]];k-=(1<<logn[k]); k-=dep[u]-dep[top[u]];u=top[u]; return k>0?l[dfn[u]+k]:r[dfn[u]-k]; } ll c[N];//对 dfs 序建立树状数组维护区间加单点查 inline void add(int x,int y){for(;x<=n;x+=x&-x)(c[x]+=y+MOD)%=MOD;} inline int ask(int x){int s=0;for(;x;x-=x&-x)(s+=c[x])%=MOD;return s;} int op,u; signed main() { n=read();q=read();inv=qpow(n,MOD-2); for(int i=1,u,v;i<n;i=-~i) u=read(),v=read(), e[u].push_back(v),e[v].push_back(u); for(int i=1;i<=n;i=-~i) logn[i]=-~logn[i>>1],siz[i]=1; get_son(1);get_chain(1,1); while(q--) { op=read();u=read(); if(op==1) { if(!in[u]) v.push_back(u),in[u]=1; (d[u]+=read())%=MOD;//将对于同一个点的修改合并 } else { ll sum=ask(dfn[u]);//前面的块的贡献 for(int i:v)//当前块的贡献 { if(u==i) sum+=d[i]*n%MOD; else if(dep[u]<=dep[i]) sum+=d[i]*siz[i];//i 子树外 else { int x=ask(u,dep[u]-dep[i]-1); if(fa[x][0]==i) sum+=d[i]*(n-siz[x])%MOD;//i 子树内 else sum+=d[i]*siz[i]%MOD;//i 子树外 } sum%=MOD; } printf("%lld\n",sum*inv%MOD); } if(v.size()==B)//根号重建 { for(int i:v) { for(int j:e[i]) if(j!=fa[i][0]) add(dfn[j],d[i]*(n-siz[j])%MOD), add(dfn[j]+siz[j],-d[i]*(n-siz[j])%MOD); add(dfn[i],d[i]*n%MOD);add(-~dfn[i],-d[i]*n%MOD); add(1,d[i]*siz[i]%MOD);add(dfn[i],-d[i]*siz[i]%MOD); add(dfn[i]+siz[i],d[i]*siz[i]%MOD); d[i]=in[i]=0; } v.clear(); } } return 0; }
posted @   Sorato  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
历史上的今天:
2023-10-15 P9310 Luna likes Love 题解
点击右上角即可分享
微信分享提示
目录