P9527 [JOISC2022] 洒水器 /AT_joisc2022_h Sprinkler
题意
给定一棵 \(n\) 个点的树,要求支持两种操作。
-
1 x d w
,使所有和点 \(x\) 之间距离 \(\le d\) 的点乘以 \(w\) 并模一个给定的数 \(L\)。 -
2 x
,查询点 \(x\) 的值。
分析
本题的代码量其实不大,就我而言,难度主要在能想到如何利用 \(d\le40\) 这个条件上。
这是一棵无根树,维护的信息比较杂,没有什么数据结构能比较好的去解决这个问题,所以理应放弃那些对数据结构奇奇怪怪的想法。
虽说如此,在同机房大佬的指点下,我们有了一个分块加猫树的做法(正确性未知,应该是对的)。但在代码突破 3KB 大关而且感觉才写了不到一半的情况下果断弃疗(有能写出来的私信我一下)。
正如前面所说,\(d\le40\) 实在太小了,小到都可以上暴力了。
很显然,如果直接修改每个节点,那 \(n\le 2\times10^5\) 会直接教你做人。但如果我们只管父亲,至多也就 \(40\) 次。
如果涉及到了儿子,就自己给自己打个标记,然后到了儿子的时候,直接只管 \(40\) 个父亲有没有标记就好了。
但直接这样做是不行的,时间复杂度有 \(O(nd^2)\) 之高,妥妥超时,所以我们需要优化。
有没有可能把 \(\le d\) 的点都只标记一次呢?苦思冥想了许久之后,有了如下做法。
设 \(s_{x,d}\) 表示点 \(x\) 的子树中距离点 \(x\) 为 \(d\) 的点的标记。我们只需要对 \(x\) 的 \(g\) 级祖先在 \(s_{x,d-g}\) 和 \(s_{x,d-g-1}\) 处打标记,这样每个点就刚刚好只标记一次了。
这样时间复杂度就被优化成了 \(O(nd)\) 的,完美通过。
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+223;
#define int long long
int n,q,L;
struct node {
int to,nxt;
} edge[maxn<<1];
int head[maxn],cnt=0;
int h[maxn],fa[maxn],s[maxn][41];
void add(int u,int v) {
cnt++;
edge[cnt].to=v;
edge[cnt].nxt=head[u];
head[u]=cnt;
}
void dfs(int u,int fath) {
fa[u]=fath;
for(int i=head[u]; i; i=edge[i].nxt) {
int v=edge[i].to;
if(v!=fath)
dfs(v,u);
}
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>L;
for(int i=1; i<n; i++) {
int u,v;
cin>>u>>v;
add(u,v);
add(v,u);
}
for(int i=1; i<=n; i++)
cin>>h[i];
for(int i=1; i<=40; i++) {
int u=n,v=++n;
add(u,v);
add(v,u);
}
for(int i=1; i<=n; i++)
for(int j=0; j<=40; j++)
s[i][j]=1;
dfs(n,0);
cin>>q;
while(q--) {
int op,x,d,w;
cin>>op;
if(op==1) {
cin>>x>>d>>w;
while(d>=0) {
s[x][d]=(s[x][d]*w)%L;
if(d>0)
s[x][d-1]=(s[x][d-1]*w)%L;
x=fa[x];
d--;
}
} else {
cin>>x;
int ans=h[x];
int d=0;
while(d<=40) {
ans=(ans*s[x][d])%L;
x=fa[x];
d++;
}
cout<<ans<<endl;
}
}
return 0;
}