CF1254D Tree Queries 题解
CF1254D Tree Queries 题解
题目大意
给定一棵 \(N\) 个节点的树,有 \(Q\) 次操作,分别是如下两种:
- 给定一个点 \(v\) 和一个权值 \(d\),等概率地选择一个点 \(r\),对每一个点 \(u\),若 \(v\) 在 \(u\) 到 \(r\) 的路径上,则\(u\)的权值加上\(d\)。
- 查询 \(v\) 的权值期望,对 \(998244353\) 取模。
Solve
考虑对节点 \(u\) 执行操作 1 之后,对树上所有节点的贡献。
- 对于 \(u\) 自己,期望权值需要加上 \(d\)。
- 对于 \(u\) 子树里(除去 \(u\) 自己)的点,若这个点在以 \(v\) 为根的子树里(\(v\in son(u)\)),那么只有选到的 \(r\) 不在 \(v\) 的子树里才对这个点有贡献,所以这个点的期望权值需要加上 \(d\cdot siz(v)\over N\)。
- 对于不在 \(u\) 子树里的点,需要选到的 \(r\) 在 \(u\) 子树里,才能对这个点有贡献,期望权值需要加上 \(d\cdot siz(u)\over N\)。
对树建立 dfs 序,那么就可以转化为区间加,用树状数组维护。
但问题是,对 \(u\) 子树里的点的贡献和那个点所在的子树有关,如果每次都遍历 \(u\) 的所有儿子,可以被菊花卡到 \(O(NQ\log_2N)\)。所以考虑一个根号分治 / 根号重建。
将操作分为若干个大小为 \(B\) 的块。记录每个块内的修改操作,把对同一个节点的修改合并。
每 \(B\) 次修改,遍历这个块内的所有修改并按上面的方式执行。由于每个点的所有边至多被遍历一次,复杂度 \(O(\frac Q B\cdot N\log_2N)\)。
对于一次询问,它所在块前面的贡献都已经通过数据结构统计出来了,只需要遍历这个块里的所有修改就行。
比如当前询问的点是 \(u\),遍历到的修改是 \((v,d)\)。还是按上面所说的分类讨论计算贡献。那么我们需要快速求出 \(u\) 是否在 \(v\) 的子树里,若在,是在 \(v\) 的哪个儿子的子树里。可以分情况讨论。
- 若 \(u=v\),特判。
- 否则若 \(u\) 的深度 \(dep(u)\geq dep(v)\),说明 \(u\) 肯定不在 \(v\) 的子树里。
- 若 \(dep(u)<dep(v)\),我们对 \(u\) 求 \(dep(v)-dep(u)-1\) 级祖先,记为 \(fa\)。如果 \(fa\) 的父亲不是 \(v\),说明 \(u\) 不在 \(v\) 的子树里。否则,\(u\) 在 \(v\) 的子树里,并且我们也已求得了它是在 \(v\) 的哪个儿子的子树里。
这样,就可以计算贡献了。时间复杂度为 \(O(QBK)\),其中 \(K\) 为求 \(k\) 级祖先的复杂度消耗。
对于求 \(k\) 级祖先,方法很多。由于我们对于不同块的操作已经带了一个 \(\log\),如果我们在这里还使用带 \(\log\) 的算法求,如树剖和倍增,那么总复杂度是在 \(B=\sqrt N\) 时取得最小值,为 \(O(Q\sqrt N \log_2N)\),较慢。实测倍增写法跑了 4000ms+。
用长链剖分求 \(k\) 级祖先,可以做到 \(O(N\log_2N)-O(1)\)。这样总复杂度在 \(B=\sqrt{N\log_2N}\) 时取得最小值 \(O(Q\sqrt{N\log_2N})\)。
Code
#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;
}