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;
}
posted @ 2023-11-12 22:32  p7gab  阅读(0)  评论(0编辑  收藏  举报  来源