点分治&&点分树

点分治

前置芝士:树的重心

树的重心指对于一棵无根树上的一点,其分割开的若干子树大小的最大值最小。

一般用 DFS 求解树的重心。初始时,mxs=sum=n,即树的节点个数。最终的 rt 即为重心。

void getrt(int u,int fno){
	int s=0;
	siz[u]=1;
	for(int i=head[u],v;i;i=e[i].to){
		if((v=e[i].v)==fno||del[v]) continue;
		getrt(v,u);
		siz[u]+=siz[v];
		s=max(s,siz[v]);
	}
	s=max(s,sm-siz[u]);
	if(s<mxs) mxs=s,rt=u;
}

点分治

点分治,又名淀粉质,是在树上进行静态统计的算法。这里的静态统计一般是树剖维护不了的,所以我们需要点分治。但在实际应用中,树剖确实能解决很多点分治的题,码量还更小。

点分治的思想是将一棵树拆成许多棵子树进行处理。比如,对于一条路径,可以将其分成两类:经过根节点和不经过根节点的路径。对于前者,可以预处理出每个点到根的路径,然后 dis(u,v)=dis(u,rt)+dis(v,rt),并排除不合法的情况(u,v 在同一子树中);对于后者,通过分治转化为前者。

如果是均衡的树,查询一次是 O(nlogn) 的;如果是一条链,查询将退化为 O(n2)。所以分治前对每棵子树找到树的重心做根即可。

P3806 【模板】点分治 1

题意:给定一棵 n 个点的树,每次询问树上距离为 k 的点对是否存在。

点分治板子题,分四步走:

  1. 找出树的重心做根,getrt()
  2. 求出子树各点到根的距离,getdis()
  3. 统计当前子树答案,calc()
  4. 分治各个子树,重复上述操作,divide()
// 核心代码
void getrt(int u,int fno){
	siz[u]=1;
	int s=0;
	for(int i=head[u],v;i;i=e[i].to){
		if((v=e[i].v)==fno||del[v]) continue;
		getrt(v,u);
		siz[u]+=siz[v];
		s=max(s,siz[v]);
	}
	s=max(s,sum-siz[u]);
	if(s<mxs) mxs=s,rt=u;
}
void getdis(int u,int fno){
	dis[++cnt]=d[u];
	for(int i=head[u],v;i;i=e[i].to){
		if((v=e[i].v)==fno||del[v]) continue;
		d[v]=d[u]+e[i].w;
		getdis(v,u);
	}
}
void calc(int u){
	judge[0]=1;
	int p=0;
	for(int i=head[u],v;i;i=e[i].to){
		if(del[v=e[i].v]) continue;
		cnt=0;
		d[v]=e[i].w;
		getdis(v,u);
		for(int j=1;j<=cnt;++j)
			for(int k=1;k<=m;++k)
				if(ask[k]>=dis[j])
					ans[k]|=judge[ask[k]-dis[j]];
		for(int j=1;j<=cnt;++j)
			if(dis[j]<MAXK) judge[q[++p]=dis[j]]=1;
	}
	for(int i=1;i<=p;++i) judge[q[i]]=0;
}
void divide(int u){
    del[u]=1;
	calc(u);
	for(int i=head[u],v;i;i=e[i].to){
		if(del[v=e[i].v]) continue;
		mxs=sum=siz[v];
		getrt(v,0);
		divide(rt);
	}
}

稍微解释一下 calc() 函数:前面提到计算经过根节点的路径时需要注意排除不合法情况,所以我们用 judge 数组存储之前子树中出现过的距离,对于一个子树枚举所有出现过的距离和询问,如果询问距离 ask(k)dis(j) 的差存在,则此询问合法。

P4178 Tree

题意:给定一棵 n 个节点的树,边有边权,求树上距离小于等于 k 的点对数量。

仍然点分治四步走。和板题不一样的地方在于边有边权,以及统计的是数量而不是是否存在。观察到既然是小于等于,可以想到树状数组维护前缀和。于是把所有出现过的距离扔进树状数组里,查询的时候查询前缀和即可。需要注意的是,最后不能忘了统计以根节点为端点的路径

// 这是我以前的码风
void calc(int u) {
	queue<int> q;
	for (auto vv : g[u]) {
		int v = vv.first, w = vv.second;
		if (del[v]) continue;
		d[v] = w;
		cnt = 0;
		getdis(v, u);
		for (int j = 1; j <= cnt; ++j) {
			if (dis[j] > K) continue;
			ans += ask(K - dis[j]);
		}
		for (int j = 1; j <= cnt; ++j) {
			if (dis[j] > K) continue;
			q.emplace(dis[j]);
			++judge[dis[j]];
			mdf(dis[j], 1);
		}
	}
	while (!q.empty()) {
		if (q.front() <= K) ans += judge[q.front()];
		mdf(q.front(), -judge[q.front()]);
		judge[q.front()] = 0;
		q.pop();
	}
}

P3085 [USACO13OPEN] Yin and Yang G

其实它更广为人知的名字是采药人的路径

题意:给出一棵 n 个点的树,每条边为黑色或白色。问满足以下条件的路径条数:路径上存在一个不是端点的点,使得两端点到该点的路径上两种颜色的边数都相等。

首先容易想到的是把边权变为 11,这样路径边权和为 0 的路径就是黑白边数相等的路径。

设一个点 i 到根的距离为 dis(i),则对于一条路径的端点 x,y,若 dis(x)=dis(y) 即为平衡。与此同时,若路径上存在一点 z 使得 dis(z)=dis(x)dis(z)=dis(y),这条路径就符合要求。

我们定义 z 为路径的 “中转点”。

对于答案的求解是本题的难点,设:

  • fi,0/1 表示对于当前根,现在正在遍历,距离根的距离为 i,找不到/找得到中转点的点的个数;
  • gi,0/1 表示对于当前根,以前遍历过,距离根的距离为 i,找不到/找得到中转点的点的个数。

g 的转移是容易的,把当前子树的 f 复制下来即可。

对于 f 的计算,我们在 DFS 的过程中开一个桶计算一个距离有没有出现过。如果当前点到根的距离出现过,则证明当前点有一个祖先和当前点的 dis 值是相同的,说明这个祖先就是前文说过的 z 点,即当前点能找到中转点;反之则不能。一遍 DFS 就可以计算 f

然后考虑用 fg 计算答案。先看计算答案的代码:

ans+=f[N][0]*(g[N][0]-1);
for(int j=-mxd;j<=mxd;++j)
    ans+=f[N+j][1]*g[N-j][1]+f[N+j][1]*g[N-j][0]+f[N+j][0]*g[N-j][1];

首先计算根是中转点的情况,此时找不到中转点的点也可以配对,同时根是端点的情况多算一遍要减去。

对于一组距离为相反数的点对,能否配对取决于这对点中的任意一个能否找到中转点。

剩下的就不是难点了。

AC Code

P3714 [BJOI2017] 树的难题

对于一个点为根,我们可以很容易计算出 dep(i) 表示深度,sum(i) 表示根节点到 i 的路径权值。对于任意两点 x,y 的路径权值就是 sum(x)+sum(y)接头的边权相同时的边权。但是判断接头边权相同比较棘手,所以我们创新性地将根节点的所有子树按照接头边权排序。之后就是套路,设 fi 表示当前颜色中距离根节点为 i 的节点数,gi 表示以前的颜色中距离根节点为 i 的节点数。在两个数组中查询 (ysonu) [ldep(y),rdep(y)] 的最大值。记为 a,b,则用 acy+sum(y)b+sum(y) 更新答案。

f 和 g 的转移都是容易的,但因为涉及到查询区间最大值,所以改成线段树。

AC Code

点分树(动态点分治)

点分树和点分治的关系仅仅在于建树时运用了点分治的思想。又因其主要解决带修问题,故又名动态点分治。

点分树

定义:利用点分治,以每个子树的重心做根,重构出来的一棵树。

实话说,点分树和原树的形态没有任何关系。有什么用?主要是两个性质:

  1. 树高严格小于等于 logn
  2. 原树两点 LCA 在点分树两点的路径上!

第一条性质最为重要,它可以让我们做出很多不可思议的事情。

比如,在点分树上做 n 遍暴力跳父亲,最终时间复杂度却是 O(nlogn)

比如,对点分树的每一个节点开一个 vector 存储子树所有节点的信息,空间复杂度却同样是 O(nlogn)

有的问题我们并不是特别关注树的形态特点,比如路径问题连通块问题关键点问题等,以路径问题为例,我们并不一定要算出两点的 LCA 才能计算距离,我们只需找到路径上的一个分割点即可。

点分树题的一般解题技巧是开两个数据结构(可以是数组、vector、线段树等),分别维护u 的贡献和对 u 的父亲的贡献。分别记为 fugu,因为点分树可以暴力跳父亲,所以我们在从 u 跳到 fa(u) 时,需要加上多出来的 ffa(u),同时减去重复算的 gu

点分树的常数巨大,写点分树必须注意常数问题。

P6329 【模板】点分树 | 震波

题意:给定一棵 n 个点的树,点有点权。每次查询距离点 x 不超过 k 的点的点权和。带修,强制在线。

建出点分树,记 u 在点分树上的父亲为 fa(u)u,v 两点在原树上的距离为 dis(u,v)

第一步是用倍增/树剖/ST 表求出树上两点间距离。建议使用树剖,因为码量小还跑得快(跑不满的 logn)。不建议使用倍增是因为复杂度是实打实的 logn,会给整体复杂度多套一个 log 导致复杂度错误。不建议使用 ST 表是因为懂的都懂。

第二步,开两棵动态开点线段树,设 sg(x,a,b) 表示在点分树上以 x 为根的子树中到 x 的原始距离在 [a,b] 之间的权值和,ch(x,a,b) 表示在点分树上以 x 为根的子树中到 fa(x) 原始距离为 [a,b] 之间的权值和。

查询时,暴力跳父亲,当查询距离节点 x 不超过 k 的节点的权值和时,先将答案加上 sg(x,0,k),再跳 x 在点分树上的所有祖先 u,答案先加上 sg(fa(u),0,kdis(x,fa(u)))再减去 ch(u,0,kdis(x,fa(u)))。减去是为了容斥掉多算的答案,手摸一下就能理解。这也是点分树的基本操作。

修改时,暴力跳父亲修改即可。

struct{
    ... // 树剖求LCA和树上两点距离,不再展示
}L;
struct{
    // 单加区查的动态开点线段树
	#define lp st[p].lc
	#define rp st[p].rc
	int rt[MAXN],tot;
	struct SegTree{
		int lc,rc,c;
	}st[MAXN<<5]; // 空间复杂度由数据规模决定
	void chg(int x,int k,int s,int t,int&p){
		if(!p) p=++tot;
		if(s==t) return st[p].c+=k,void();
		int mid=(s+t)>>1;
		if(x<=mid) chg(x,k,s,mid,lp);
		else chg(x,k,mid+1,t,rp);
		st[p].c=st[lp].c+st[rp].c;
	}
	int query(int l,int r,int s,int t,int p){
		if(!p) return 0;
		if(l<=s&&t<=r) return st[p].c;
		int mid=(s+t)>>1,res=0;
		if(l<=mid) res+=query(l,r,s,mid,lp);
		if(mid<r) res+=query(l,r,mid+1,t,rp);
		return res;
	}
    // 建树略有不同,wc是根,d是距离
    // 相当于给线段树上距离根节点d的位置加上了点权w[u]
	void build(int u,int fno,int wc,int d){
		chg(d,w[u],0,n,rt[wc]);
		for(int i=head[u];i;i=e[i].to)
			if(e[i].v^fno&&!del[e[i].v])
				build(e[i].v,u,wc,d+1);
	}
}sg,ch;
struct{
	int sum,mxs,rt,siz[MAXN];
	int dis[MAXN][21],dep[MAXN],fa[MAXN];
	void getrt(int u,int fno){
		siz[u]=1;
		int s=0;
		for(int i=head[u],v;i;i=e[i].to){
			if((v=e[i].v)==fno||del[v]) continue;
			getrt(v,u);
			siz[u]+=siz[v];
			s=max(s,siz[v]);
		}
		s=max(s,sum-siz[u]);
		if(s<mxs) mxs=s,rt=u;
	}
	void build(int u){
		del[u]=1;
        // sg建树时根节点是u,维护的信息是到u的信息
		sg.build(u,0,u,0);
		for(int i=head[u],v;i;i=e[i].to){
			if(del[v=e[i].v]) continue;
			sum=mxs=siz[v];
			getrt(v,0);
            // ch建树时根节点是子树重心,但维护的距离是到根节点父亲的距离
			ch.build(v,0,rt,1);
			fa[rt]=u;
			dep[rt]=dep[u]+1;
			build(rt);
		}
	}
	void init(){
        // 找重心
		sum=mxs=n;
		getrt(1,0);
		build(rt);
        // 树剖预处理
		L.dfs(1,0);
		L.dfs2(1,1);
        // 预处理dis,dis[i][j]表示点分树上i的j级祖先在原树上的距离
		for(int i=1;i<=n;++i)
			for(int j=i;j;j=fa[j])
				dis[i][dep[i]-dep[j]]=L.getdis(i,j);
	}
	int query(int x,int y){
		int res=sg.query(0,y,0,n,sg.rt[x]);
		for(int i=x,d;fa[i];i=fa[i]){
			d=dis[x][dep[x]-dep[fa[i]]];
			res+=sg.query(0,y-d,0,n,sg.rt[fa[i]]);
			res-=ch.query(0,y-d,0,n,ch.rt[i]);
		}
		return res;
	}
	void chg(int x,int y){
        // 之所以y-w[x]是因为题目中是“修改为”,此时加上差值就等于修改为
		sg.chg(0,y-w[x],0,n,sg.rt[x]);
		for(int i=x,d;fa[i];i=fa[i]){
			d=dis[x][dep[x]-dep[fa[i]]];
			sg.chg(d,y-w[x],0,n,sg.rt[fa[i]]);
			ch.chg(d,y-w[x],0,n,ch.rt[i]);
		}
	}
}P;

int main(){
	n=read(),m=read();
	for(int i=1;i<=n;++i) w[i]=read();
	for(int i=1,u,v;i<n;++i){
		u=read(),v=read();
		addedge(u,v),addedge(v,u);
	}
	P.init();
	int lst=0;
	while(m--){
		int op=read(),x=read(),y=read();
		x^=lst,y^=lst;
		if(!op) write(lst=P.query(x,y));
		else P.chg(x,y),w[x]=y;
	}
	return fw,0;
}

P3345 [ZJOI2015] 幻想乡战略游戏

题意:动态求一棵树的带点权、边权重心。

本题考察对点分树的理解。观察到如果当前点不是带权重心,那么一定有一个儿子会比它更优,查询时不断像这样跳儿子就行。又看到查询是动态的,于是考虑点分树。建好点分树之后我们可以暴力算出每一个点作为根时的答案,把儿子的答案和当前点的答案作比较,如果有儿子更优就往儿子跳即可。

于是问题转化为已知一个点求树上所有点到这个点的带权距离。具体实现就是套路了:设 sum(i) 表示点分树上 i 点子树的点权和,smd(i) 表示 i 点子树所有点到 i 的距离和,smf(i) 表示 i 点子树所有点到 fa(i) 的距离和。同样,修改时暴力跳父亲,查询时容斥查询即可。

ll sum[MAXN],smd[MAXN],smf[MAXN];
void getrt(int u,int fno){
	siz[u]=1;
	int s=0;
	for(int i=head[u],v;i;i=e[i].to){
		if((v=e[i].v)==fno||del[v]) continue;
		getrt(v,u);
		siz[u]+=siz[v];
		s=max(s,siz[v]);
	}
	s=max(s,sm-siz[u]);
	if(s<mxs) mxs=s,rt=u;
}
void build(int u){
	del[u]=1;
	for(int i=head[u],v;i;i=e[i].to){
		if(del[v=e[i].v]) continue;
		mxs=sm=siz[v];
		getrt(v,u);
		fa[rt]=u;
		g[u].emplace_back(v,rt);
		build(rt);
	}
}
void mdf(int u,ll k){
	sum[u]+=k;
	for(int i=u;fa[i];i=fa[i]){
		ll ds=L.getdis(u,fa[i]);
		sum[fa[i]]+=k;
		smd[fa[i]]+=k*ds;
		smf[i]+=k*ds;
	}
}
ll count(int u){
	ll res=smd[u];
	for(int i=u;fa[i];i=fa[i]){
		ll ds=L.getdis(u,fa[i]);
		res+=(sum[fa[i]]-sum[i])*ds;
		res+=smd[fa[i]]-smf[i];
	}
	return res;
}
ll query(int u){
	ll x=count(u);
	for(auto vv:g[u])
		if(count(vv.first)<x)
			return query(vv.second);
	return x;
}

int main(){
	n=read(),Q=read();
	for(int i=1,u,v,w;i<n;++i){
		u=read(),v=read(),w=read();
		addedge(u,v,w),addedge(v,u,w);
	}
	mxs=sm=n;
	getrt(1,0);
	int RT=rt;
	build(rt);
	L.dfs(1,0);
	L.dfs2(1,1);
	while(Q--){
		int x=read(),y=read();
		mdf(x,y);
		write(query(RT));
	}
	return fw,0;
}

P3241 [HNOI2015] 开店

先考虑如果没有 L,R 的限制怎么做。不用考虑,就是上一道题。

那么加上 L,R 的限制呢?

其实这个扩展是很巧妙的,利用了点分树的第二条性质。我们把原来的 smdsmf 改成 vector 数组,把原来的 += 改成 emplace_back 一个点的点权信息和贡献信息。这样处理完毕之后,把每个点的 vector 按照点权排序,再把第二维前缀和一下,于是查询的时候只需用 lower_boundupper_bound 锁定区间 [L,R],前缀和一减就是答案。至于 sum,我们可以在查询出 L1R 之后同样一减就是答案。最后统计答案就是老套路了。

这题有

int mxs,sm,rt,siz[MAXN];
bool del[MAXN];
void getrt(int u,int fno){
	int s=0;
	siz[u]=1;
	for(int i=head[u],v;i;i=e[i].to){
		if((v=e[i].v)==fno||del[v]) continue;
		getrt(v,u);
		siz[u]+=siz[v];
		s=max(s,siz[v]);
	}
	s=max(s,sm-siz[u]);
	if(s<mxs) mxs=s,rt=u;
}
struct PIL{
	int first;
	ll second;
	PIL(int u,int v):first(u),second(v){}
	bool operator<(const PIL&x)const{
		return first<x.first;
	}
};
vector<PIL>smd[MAXN],smf[MAXN];
int fa[MAXN];
void dfs(int u,int fno,ll w){
	smd[rt].emplace_back(x[u],w);
	if(fa[rt]) smf[rt].emplace_back(x[u],L.getdis(u,fa[rt]));
	for(int i=head[u],v;i;i=e[i].to){
		if(del[v=e[i].v]||v==fno) continue;
		dfs(v,u,w+e[i].w);
	}
}
void build(int u){
	del[u]=1;
	dfs(u,0,0);
	for(int i=head[u],v;i;i=e[i].to){
		if(del[v=e[i].v]) continue;
		mxs=sm=siz[v];
		getrt(v,0);
		getrt(rt,0);
		fa[rt]=u;
		build(rt);
	}
}
ll query(int opt,int u,int l,int r,int&sz){
	int L,R;
	if(!opt){
		L=lower_bound(smd[u].begin(),smd[u].end(),PIL(l,0ll))-smd[u].begin()-1;
		R=upper_bound(smd[u].begin(),smd[u].end(),PIL(r,0ll))-smd[u].begin()-1;
	}else{
		L=lower_bound(smf[u].begin(),smf[u].end(),PIL(l,0ll))-smf[u].begin()-1;
		R=upper_bound(smf[u].begin(),smf[u].end(),PIL(r,0ll))-smf[u].begin()-1;
	}
    // 这个L实际上是L-1所在位置,R就是R的位置
	sz=R-L;
	ll res=0;
	if(!opt){
		if(R>=0&&R<(int)smd[u].size()) res+=smd[u][R].second;
		if(L>=0&&L<(int)smd[u].size()) res-=smd[u][L].second;
	}else{
		if(R>=0&&R<(int)smf[u].size()) res+=smf[u][R].second;
		if(L>=0&&L<(int)smf[u].size()) res-=smf[u][L].second;
	}
	return res;
}

int main(){
	n=read(),Q=read(),A=read();
	for(int i=1;i<=n;++i) x[i]=read();
	for(int i=1,u,v,w;i<n;++i){
		u=read(),v=read(),w=read();
		addedge(u,v,w),addedge(v,u,w);
	}
	L.dfs(1,0);
	L.dfs2(1,1);
	sm=mxs=n;
	getrt(1,0);
	getrt(rt,0);
	build(rt);
	for(int i=1;i<=n;++i){
		sort(smd[i].begin(),smd[i].end());
		sort(smf[i].begin(),smf[i].end());
		if(smd[i].size()>1)
			for(auto it=next(smd[i].begin());it!=smd[i].end();++it)
				it->second+=prev(it)->second;
		if(smf[i].size()>1)
			for(auto it=next(smf[i].begin());it!=smf[i].end();++it)
				it->second+=prev(it)->second;
	}
	ll lst=0;
	while(Q--){
		int u=read(),l=(read()+lst)%A,r=(read()+lst)%A,sz1,sz2;
		if(l>r) swap(l,r);
		lst=query(0,u,l,r,sz1);
		for(int i=u;fa[i];i=fa[i]){
			lst+=query(0,fa[i],l,r,sz2)-query(1,i,l,r,sz1);
			lst+=(sz2-sz1)*L.getdis(fa[i],u);
		}
		write(lst);
	}
	return fw,0;
}
posted @   Laoshan_PLUS  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示