P9527 [JOIST 2022] 洒水器 / Sprinkler 题解

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;
}
相关推荐
kiracrimson1 小时前
希尔排序:让插入排序不再怕逆序
数据结构·算法·排序算法
垆边人似月.2 小时前
华为机考题:最小代价爬楼梯
c语言·c++
可涵不会debug2 小时前
C++ std::bind 实战教程:线程池、回调场景全覆盖
linux·开发语言·c++
yi0112 小时前
DAY17: LeetCode 139|单词拆分:从“把单词删掉”到用 DP 记录合法切口
人工智能·笔记·python·算法·leetcode·动态规划
陈希瑞2 小时前
LongCat-2.5-Preview 的web逆向能力实测
人工智能·算法·wasm
垆边人似月.3 小时前
华为机考题:二叉树的最大路径和
c语言·c++
2601_962218613 小时前
深入理解C++中的thread_local线程局部变量的应用
开发语言·c++
无名猿3 小时前
C++ lambda 捕获列表详解:值捕获、引用捕获、初始化捕获与悬垂引用
c++·现代c++·语法基础·踩坑记录
tangguofeng4 小时前
基于分治策略的循环赛日程表问题的思路讲解
算法