点分树

P6329 【模板】点分树 | 震波

就就着这个模板题讲一下吧

题意:求在\(x\)距离\(k\)以内的所有点的点权,点权可以修改

先考虑简化问题:只处理以一个固定点为中心的答案

我们需要用一些数据结构维护一些信息,维护什么呢?

考虑查询时是查询到这个点的距离为\(0- k\)的点权之和,即求前缀和

那么维护的信息就是:到当前根的距离在\([0,k]\)范围内的点的点权,每次修改是单点修改,查询前缀和

那么这道题可以用树状数组(因为边权为\(1\),所以取值一定是连续的,不存在空的点浪费空间的情况)

但是可能边权稍微大了一点树状数组空间就会爆炸,那么就要用动态开点线段树(好像没啥用)

struct arr{
	vector<int> v;
	inline int& operator[](int x){//注意到每个树状数组的大小不一样,需要动态开(下面会说到)
		while(x>=v.size())v.push_back(0);
		return v[x];
	}
};
inline int lowbit(int x){return x&(-x);}
struct BIT{
	arr val;
	inline void modify(int n,int pos,int x){//pos可以取0,但是树状数组不支持,必须同时加上一个1
		++n;++pos;while(pos<=n)val[pos]+=x,pos+=lowbit(pos);	
	}
	inline int query(int n,int pos){
		++pos;pos=min(n+1,pos);int ret=0;
		while(pos)ret+=val[pos],pos-=lowbit(pos);
		return ret;
	}
}T1[N],T2[N];

接下来考虑多个点询问的情况

那么每个点的改变都会导致所有节点的树状数组发生改变,暴力修改是\(O(n^2\log n)\)

那么显然不能更改全部的点,那么只能更改一部分查询的时候加和

那么就需要容斥一下

中间的部分被算了两次,需要被剪掉一次

根据点分治的基本思想,每次把树分成\(\log\)的大小

样例的这个图

按这个找根的顺序重新建树

最大的树的重心\(3\)控制所有点

然后\(2\)控制\(2,3,4,5\)

以此类推

namespace Divide{
	int size[N],sz[N],rt,mn;
	bool vis[N];
	void get_size(int u,int fa){
		size[u]=1;for(int i=head[u],v;i;i=e[i].next){
			v=e[i].to;if(v==fa||vis[v])continue;
			get_size(v,u);size[u]+=size[v];
		}
	}
	int get_dep(int u,int fa,int d){
		int ret=d+1;for(int i=head[u],v;i;i=e[i].next){
			v=e[i].to;if(v==fa||vis[v])continue;
			ret=max(ret,get_dep(v,u,d+1));
		}
		return ret;
	}
	void dfs1(int u,int fa,int S){
		int cur=S-size[u];for(int i=head[u],v;i;i=e[i].next){
			v=e[i].to;if(v==fa||vis[v])continue;
			dfs1(v,u,S);cur=max(cur,size[v]);
		}
		if(cur<mn)mn=cur,rt=u;
	}
	inline int root(int u){get_size(u,u);mn=inf,rt=u;dfs1(u,u,size[u]);return rt;}
	int fa[N];
	void build(int u,int father){
		int x=root(u);
		sz[x]=get_dep(x,x,-1);fa[x]=father;u=x;vis[u]=1;
		for(int i=head[u],v;i;i=e[i].next){
			v=e[i].to;if(vis[v]||v==father)continue;
			build(v,u);
		}
	}
}

修改与查询操作

  • 查询

先看需要查询哪些东西,再去看要维护些啥

首先在重新建的树中依次跳父亲

起点:自己,直接把与自己距离小于等于\(k\)的加上即可

然后依次跳父亲,再加上与自己距离小于等于\(k-dis(cur,start)\)的点的点权

但是有一部分是既在与上一层\(k\)的距离以内,又在现在这一层\(k-dis(cur,start)\)的距离以内,必须减掉。即减去下一层子树中到当前根的距离小于等于\(k-dis(cur,start)\)的权值和

  • 距离\(dis\)\(ST\)\(O(1)\)求法

每次\(O(\log n)\)找LCA太慢,用一种\(O(n\log n)\)预处理,\(O(1)\)查询的ST表方法

对树进行DFS

那么查询\(x,y\)间的LCA:查询两个点第一次出现\([id[x],id[y]]\)间的深度最小的那个点,这个点就是LCA

namespace Graph{
	int inde=0,st[N<<2][LOG],Log2[N<<2],dep[N],id[N];
	inline void dfs(int u,int fa){
		st[++inde][0]=u;dep[u]=dep[fa]+1;id[u]=inde;
		for(int i=head[u],v;i;i=e[i].next){v=e[i].to;if(v==fa)continue;dfs(v,u);st[++inde][0]=u;}
	}
	inline int get_min(int x,int y){return dep[x]<dep[y]?x:y;}
	inline void pre_work(){
		for(int i=1;i<=inde;++i)Log2[i]=Log2[i-1]+(1<<Log2[i-1]==i);
		for(int i=1;i<Log2[inde];++i)
			for(int j=1;j+(1<<i)<=inde;++j)
				st[j][i]=get_min(st[j][i-1],st[j+(1<<(i-1))][i-1]);
	}
	inline int LCA(int x,int y){
		if(x==y)return x;
		x=id[x],y=id[y];if(x>y)swap(x,y);
		int l=Log2[y-x+1]-1;
		return get_min(st[x][l],st[y-(1<<l)+1][l]);
	}
	inline int dis(int x,int y){return dep[x]+dep[y]-2*dep[LCA(x,y)];}
	inline void init(){dfs(1,0);pre_work();}
}

至此完成了\(query\)

inline int query(int x,int k){
	int ans=0,start=x,pre=0;
	while(x){
		int dis=Graph::dis(x,start);
		if(dis<=k){
			ans+=T1[x].query(k-dis);
			if(pre)ans-=T2[pre].query(k-dis);
		}
		pre=x,x=fa[x];
	}
	return ans;
}
  • 修改

对于每个点,依次修改控制得到它的所有重心的贡献

那么按照上面的查询的需要,更新当前子树的重心的同时,还要更新上一层更大子树的根的贡献(为了去重)

inline void update(int x,int val){
	int start=x;
	while(x){
		T1[x].modify(sz[x],Graph::dis(start,x),val);
		if(fa[x])T2[x].modify(sz[fa[x]],Graph::dis(start,fa[x]),val);
		x=fa[x];
	}
}

主函数中:

for(int i=1,u,v;i<n;++i)u=read(),v=read(),adde(u,v),adde(v,u);
Graph::init();
build(1,0);
for(int i=1;i<=n;i++)update(i,value[i]);
int lastans=0;
int opt,x,y;
while(m--){
	opt=read(),x=read()^lastans,y=read()^lastans;
	if(opt==0)printf("%d\n",lastans=query(x,y));
	else{
		int delta=y-value[x];
		update(x,delta);
		value[x]=y;
	}
}
posted @ 2021-01-09 13:59  harryzhr  阅读(110)  评论(0编辑  收藏  举报