P9527 JOIST 2022 洒水器 / Sprinkler 题解
注意力惊人
思路
首先,我们注意到 Dk≤40D_k\le40Dk≤40,所以考虑暴力一些的解法。
每次直接遍历所有相邻的点肯定是不行的,但我们注意到与每个节点距离不超过 DkD_kDk 的祖先至多有 DkD_kDk 个,所以考虑对祖先进行操作。
考虑操作一个点会对哪些点产生影响。
首先肯定会对子树内的点产生影响,设 tagx,itag_{x,i}tagx,i 表示对 xxx 的子树内与 xxx 距离不超过 iii 的点的标记,那么直接对 tagx,dtag_{x,d}tagx,d 加标记即可。
然后考虑对祖先及其子树内节点的影响。

注:图片及后文的 ddd 是指当次修改的范围 DkD_kDk。
如图所示,我们只需要对 tagfx,d−(depx−depfx)tag_{fx,d-(dep_x-dep_{fx})}tagfx,d−(depx−depfx) 乘上 WWW 并对 tagy,d−(depx−depfx)−1tag_{y,d-(dep_x-dep_{fx})-1}tagy,d−(depx−depfx)−1 除去 WWW 即可。
然后每次查询就只需要暴力往上跳祖先累计答案即可。
但是此时有个严峻的问题,就是题目没有保证模数一定与 WWW 互质,那么我们就无法使用逆元了,而且常数还很大,所以我们要找到一种方法每次修改查询只用乘。
我们注意似乎每次打标记都会打很多重复的标记,考虑修改 xxx 时,我们每次修改它的祖先,每个祖先 yyy (除根节点外)都会被修改两次:一次是给以它为根的子树打上标记,一次是给它父节点去掉 x−fxx-fxx−fx 的标记。第一次修改的是将 tagy,d−(depx−depy)tag_{y,d-(dep_x-dep_y)}tagy,d−(depx−depy) 乘 WWW,第二次修改的是将 tagy,d−(depx−depfx)−1tag_{y,d-(dep_x-dep_{fx})-1}tagy,d−(depx−depfx)−1,即 tagy,d−(depx−depy)−2tag_{y,d-(dep_x-dep_y)-2}tagy,d−(depx−depy)−2 除 WWW,这似乎像一个前缀和形式,于是此时我们去掉重复修改的部分(即 0,d−(depx−depy)−20,d-(dep_x-dep_y)-20,d−(depx−depy)−2),惊人的发现:此次修改只对 yyy 的子树中与 yyy 的距离在 d−(depx−depy)−1,d−(depx−depy)d-(dep_x-dep_y)-1,d-(dep_x-dep_y)d−(depx−depy)−1,d−(depx−depy) 的节点有影响!而且对于 xxx 本身也是符合这个条件的!
所以我们将 tagx,itag_{x,i}tagx,i 的状态改为对 xxx 的子树内与 xxx 距离恰好 为 iii 的点的标记,那么每次修改时只需要把 xxx 及其祖先的 tagy,d−(depx−depy)tag_{y,d-(dep_x-dep_y)}tagy,d−(depx−depy) 和 tagy,d−(depx−depy)−1tag_{y,d-(dep_x-dep_y)-1}tagy,d−(depx−depy)−1 改一下即可。需要注意的是,对于根节点,由于它没有父节点,所以他的标记还是距离为 1,d−(depx−depy)1,d-(dep_x-dep_y)1,d−(depx−depy) 的,所以对于根节点要全部距离 ≤d−(depx−depy)\le d-(dep_x-dep_y)≤d−(depx−depy) 的 tagrttag_{rt}tagrt 都要修改一遍。
查询就暴力往上跳 ddd 个祖先 yyy,每次记录一下 tagy,depx−depytag_{y,dep_x-dep_y}tagy,depx−depy 即可。
由于 d≤40d\le 40d≤40,每次跳的祖先不超过 40,对于修改的根节点最多只有一个,最多改 ddd 个距离的标记,所以单次修改和查询的时间复杂度都为 O(d)O(d)O(d),总时间复杂度为 O(n+qd)O(n+qd)O(n+qd)。
代码
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,mod;//节点数量及模数
vector<int> G[200010];
ll h[200010]/*初始每个点的高度*/,tag[200010][42]/*题解中的tag数组*/;
int fa[200010];//每个点的父节点
void dfs(int x,int xfa){//记录每个节点的父节点
fa[x]=xfa;
for(int y:G[x])if(y!=xfa){
dfs(y,x);
}
}
void solve(int X,int d,int val){//修改
int x=X;
while(d>=0&&x){
tag[x][d]=tag[x][d]*val%mod;//修改tag[y][d-(dep[x]-dep[y])]
if(d-1>=0)tag[x][d-1]=tag[x][d-1]*val%mod;//修改tag[y][d-(dep[x]-dep[y])-1]
if(!fa[x])for(int i=0;i<=d-2;i++)tag[x][i]=tag[x][i]*val%mod;//特判根节点的情况
x=fa[x];//往上跳
d--;
}
}
ll query(int x){//查询
ll ans=h[x];
for(int i=0;i<=40&&x;i++){//往上跳40个祖先
ans=ans*tag[x][i]%mod;//记录当前祖先对x的标记
x=fa[x];//往上跳
}
return ans;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>mod;
for(int i=1,x,y;i<n;i++){
cin>>x>>y;
G[x].push_back(y);
G[y].push_back(x);
}
dfs(1,0);
for(int i=1;i<=n;i++)cin>>h[i];
for(int i=1;i<=n;i++)for(int j=0;j<=40;j++)tag[i][j]=1;//记得初始化为1
int Q;
cin>>Q;
while(Q--){
int op;
cin>>op;
if(op==1){
int x,d,v;
cin>>x>>d>>v;
solve(x,d,v);
}
else{
int x;
cin>>x;
cout<<query(x)<<'\n';
}
}
return 0;
}