(笔记)点分治 点分树 动态点分治

基础知识

树的重心

定义

定义一颗无根树,其中节点 \(u\) 的子节点编号分别为 \([1,n]\) ,且定义 \(siz_i\) 为节点 \(i\) 的以 \(u\) 为根下计算的子树大小, \(f_u=\max(siz_1,siz_2,...,siz_n)\)
重心即找到一个点 \(u\) 使上述 \(f_u\) 的值尽可能小。

性质

  1. 假设树的重心 \(root\) 的所有儿子当中,以儿子 \(i\) 为根的子树的节点数最多,记为 \(siz_i\) ,那么\(2siz_i \le size_{root}\)

点分治

一种通过在树上,将一个点问题拆分为多个部分解决的办法,其中一个点一般指树的重心,即可使树按照重心拆分后子树尽可能平衡,从而降低时间复杂度。

点分治的本质就是删点,每次在母树中找到重心,完成计算后将重心删去,然后在其所有子树内再分别寻找其重心,重复上述操作。由于性质1,递归层数不会超过 \(O(\log n)\),每层 \(O(n)\),总共 \(O(n\log n)\)

例题

CF161D Distance in Tree

题意概要:给出一棵无根树,边权都是1,求有多少条简单路径的长度等于k,路径长度是指经过的边的权值之和。

下述三个函数,void fr用于寻找重心,void gdsgetDistance,统计树内所有节点到根节点的距离,并记录在数组 \(q\) 中。void solve即分治过程,\(cnt_i\) 表示到重心距离为 \(i\) 的节点数量。

代码贴贴:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
int n,head[maxn],idx,siz[maxn],bcs,rt,L;
int q[maxn],p,cnt[maxn],k;
long long ans;
bool vis[maxn];
struct Edge{
	int v,next;
}e[2*maxn];
void ins(int x,int y){
	e[++idx].v=y;
	e[idx].next=head[x];
	head[x]=idx;
}
void fr(int u,int fa,int nodeCnt){
	siz[u]=1;
	int bs=0;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v;
		if(v==fa||vis[v])continue;
		fr(v,u,nodeCnt);
		siz[u]+=siz[v];
		if(siz[v]>bs)
			bs=siz[v];
	}
	if(nodeCnt-siz[u]>bs)
		bs=nodeCnt-siz[u];
	if(bs<bcs){
		rt=u;
		bcs=bs;
	}
}
void gds(int u,int fa,int dis){
	q[++p]=dis;
	siz[u]=1;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v;
		if(v==fa||vis[v])continue;
		gds(v,u,dis+1);
		siz[u]+=siz[v];
	}
}
void solve(int u,int fa,int nodeCnt){
	rt=u;
	bcs=nodeCnt-1;
	fr(u,fa,nodeCnt);
	p=L=1;
	q[1]=0;
	cnt[0]=1;
	vis[rt]=true;
	for(int i=head[rt];i;i=e[i].next){
		int v=e[i].v;
		if(v==fa||vis[v])continue;
		gds(v,rt,1);
		for(int j=L+1;j<=p;j++)
			if(k>=q[j])
				ans+=cnt[k-q[j]];
		for(int j=L+1;j<=p;j++)
			cnt[q[j]]++;
		L=p;
	}
	for(int j=1;j<=p;j++)
		cnt[q[j]]--;
	for(int i=head[rt];i;i=e[i].next){
		int v=e[i].v;
		if(v==fa||vis[v])continue;
		solve(v,rt,siz[v]);
	}
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>k;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		ins(u,v);
		ins(v,u);
	}
	solve(1,0,n);
	cout<<ans;
	return 0;
}

P5306 [COCI 2018/2019 #5] Transport

古早测试题,当时 Soh_paraMEEMS 大神场切紫,令人敬畏。诸多评论说是点分治板子,实则还是需要一点思维量的。很容易想到要在每棵有根树上对于每个节点 \(v\) 处理出从根到 \(v\) 需要的最少燃油 \(les_v\)\(v\) 到根节点最多能剩 \(gan_v\)\(v\) 能否到根节点?如果能,\(need_v\leftarrow 0\),否则 \(need_v\)\(v\) 到根节点最少仍需多少燃油补充。

注意到只有 \(need_v=0\) 时才能计算 \(gan_v\),只需要分别转移即可,转移过程建议读者手推,并不难。当然这只是一种状态设计,还有更加简明的但是这只是阐述个人思路。

接下来要解决的就是匹配问题。对于 \(u\) 为根子树的两个互异且有序节点 \(v_1,v_2\),它们可以匹配当且仅当 \(gan_{v_1}\geq les_{v_2}\)。这个东西可以先跑一遍点分治,离散化下来然后树状数组什么的处理。但是也可以考虑更加简单的方式,直接容斥计算。只需要双指针先推出总的匹配情况,然后针对 \(u\) 的每个直接儿子 \(v\) 的子树,双指针推出其内部的匹配情况并在总答案上减去即可。加上排序时间复杂度 \(O(n\log^2 n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+5;
const LL INF=1e14;
int n,a[N],rt,siz[N],mxsiz[N];
int head[N],idx,nsiz;
struct Edge{int v,next,w;}e[N<<1];
bool vis[N];
void ins(int x,int y,int z){
	e[++idx].v=y;
	e[idx].next=head[x];
	e[idx].w=z;
	head[x]=idx;
}
void fr(int u,int fa){
	siz[u]=1;
	mxsiz[u]=0;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v;
		if(v==fa)continue;
		if(vis[v])continue;
		fr(v,u);
		siz[u]+=siz[v];
		mxsiz[u]=max(mxsiz[u],siz[v]);
	}
	mxsiz[u]=max(mxsiz[u],nsiz-siz[u]);
	if(mxsiz[u]<mxsiz[rt])rt=u;
}
LL ans,les[N],gan[N],need[N],god;
vector<LL>q1,q2,s1,s2;
void gd(int u,int fa,LL dis,LL rec){
	if(!need[u])s1.push_back(gan[u]),q1.push_back(gan[u]);
	s2.push_back(les[u]),q2.push_back(les[u]);
	siz[u]=1;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v,w=e[i].w;
		if(v==fa||vis[v])continue;
		LL disv=dis+w;
		LL recv=rec+a[v];
		les[v]=max(les[u],disv-rec);
		if(a[v]-w>=need[u])need[v]=0;//转移 
		else need[v]=max(disv-recv,need[u]-(a[v]-w));
		gan[v]=god+recv-disv;
		gd(v,u,disv,recv);
		siz[u]+=siz[v];
	}
}
void solve(int u){
	vis[u]=1;god=a[u];
	int pos=-1;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v,w=e[i].w;
		if(vis[v])continue;
		les[v]=w;
		need[v]=max(0,w-a[v]);
		gan[v]=god+a[v]-w;
		gd(v,u,w,a[v]);
		sort(s1.begin(),s1.end());
		sort(s2.begin(),s2.end());
		pos=-1;
		for(LL p1:s1){
			while(pos+1<(int)s2.size()&&s2[pos+1]<=p1)pos++;
			ans-=pos+1;
		}
		s1.clear();s2.clear();
	}
	q1.push_back(god);
	q2.push_back(0);
	sort(q1.begin(),q1.end());
	sort(q2.begin(),q2.end());
	pos=-1;
	for(LL p1:q1){
		while(pos+1<(int)q2.size()&&q2[pos+1]<=p1)pos++;
		ans+=pos+1;
	}
	ans--;
	q1.clear();q2.clear();
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v,w=e[i].w;
		if(vis[v])continue;
		nsiz=siz[v];rt=0;
		fr(v,u);
		solve(rt);
	}
}
int main(){
	mxsiz[0]=1e9;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	for(int i=1;i<n;i++){
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		ins(u,v,w);ins(v,u,w);
	}
	nsiz=n;rt=0;
	fr(1,0);
	solve(rt);
	printf("%lld",ans);
	return 0;
}

P9058 [Ynoi2004] rpmtdq

其实没什么好说的,高质量题解 P9058【[Ynoi2004] rpmtdq】都已经说完了。

挺好的题,trick 是找支配点对。具体来说,对于 \((l,r)\),如果存在 \(l\le l'\land r'\le r\land l'\neq r'\)\((l',r')\),且 \(dis(l,r)\geq(l',r')\),显然这个点对 \((l,r)\) 是无效点对,因为只要能取到 \((l,r)\) 就能取到更优的 \((l',r')\)

点对问题考虑点分治,观察有效点对的性质,发现记 \(p\) 距离分治中心 \(dis_p\),那么 \(dis(a,b)\le dis_a+dis_b\),如果不在同一子树地取等。我们近似地认为它们相等,对统计答案所造成的影响分两个角度考虑:第一是作为可能的支配点对会变得非法,事实上支配点对一定会在某个分治中心取等,所以如果一开始没有取等说明在当前中心就不优;第二是作为非法点对可能会限制合法点对,但是显然取到的某些 \(dis_u\) 会变大,所以限制是更松的。

对于一个支配点对 \((l,r)\),其满足 \(l < k < r\) 的更劣点对 \((l,k)(k,r)\) 一定满足:\(dis(l,k)>dis(l,r)\land dis(k,r)>dis(l,r)\),且这是一个必要条件。可以转化为 \(dis_k>dis_r\land dis_k>dis_l\),那么扩展到 \(k\in[l+1,r-1]\) 就需要满足 \(\max(dis_l,dis_r)<\min_{k\in(l,r)}dis_k\)

每层分治计算出 \(dis_u\),按编号从小到大假定枚举的 \(v\)\(dis_l,dis_r\) 中比较大的那个 \(dis_v\),那直接按编号往前找到第一个 \(dis_u\le dis_v\)\(u\) 这样 \((u,v)\) 就是一个合法支配点对。同理,往后找第一个 \(dis_u\le dis_v\)\(u\) 这样 \((v,u)\) 也是合法支配点对,可以用单调栈前后扫两遍简单维护。由于需要排序这里多带个 \(\log\)。这样计算出来点对数量是 \(O(n\log n)\) 的,因为每个点在每个包含它的分治只会加入 \(O(1)\) 组合法点对。

为什么不再往前/往后取呢?试想如果你都能直接取到前面了,那把 \(v\) 换成之前找到的第一个 \(u\) 不是更优吗?

最后点对转化为坐标系上带权点 \((l,r,dis(l,r))\),对于区间 \([l',r']\) 的贡献发现不能简单扫描线因为 \(\min\) 信息不可差分,但是发现这是一个简单二维偏序问题,那么直接 \(r\) 维升序扫,然后每次查询 \(l\) 单点 \(\text{checkmin}\),树状数组后缀 \(\min\) 查询即可(前缀 \(\min\) 下标倒过来)。

时间复杂度 \(O(n\log^2 n+p\log n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
typedef set<int>::iterator IT;
const int N=1e6+5;
const LL INF=2e14;
bool vis[N];
int n,idx,head[N],top[N],fat[N],rt;
int siz[N],son[N],qn,mxsiz[N],tot;
struct Edge{int v,next,w;}e[N<<1];
LL dep[N],ans[N];
vector<PII>q[N];
vector<int>op[N];
void dfs0(int u,int fa){
	fat[u]=fa;siz[u]=1;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v,w=e[i].w;
		if(v==fa)continue;
		dep[v]=dep[u]+w;
		dfs0(v,u);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]])
			son[u]=v;
	}
}
void dfs1(int u){
	int fa=fat[u];
	if(son[fa]==u)top[u]=top[fa];
	else top[u]=u;
	if(son[u])dfs1(son[u]);
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v;
		if(v==fa||v==son[u])continue;
		dfs1(v);
	}
}
LL Dis(int x,int y){
	int orx=x,ory=y,lc=0;
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		x=fat[top[x]];
	}
	if(dep[x]>dep[y])swap(x,y);
	lc=x;
	return dep[orx]+dep[ory]-2*dep[lc];
}
void link(int x,int y,int z){
	e[++idx].v=y;
	e[idx].next=head[x];
	e[idx].w=z;
	head[x]=idx;
}
struct BIT{
	LL av[N];
	inline int lowbit(int x){return x&-x;}
	void init(){
		for(int i=1;i<=n;i++)
			av[i]=INF;
	}
	void chkmin(int p,LL x){
		p=n-p+1;
		for(int i=p;i<=n;i+=lowbit(i))
			av[i]=min(av[i],x);
	}
	LL que(int p){
		p=n-p+1;LL res=INF;
		for(int i=p;i;i-=lowbit(i))
			res=min(res,av[i]);
		return res;
	}
}T;
void fr(int u,int fa){
	siz[u]=1;
	mxsiz[u]=0;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v;
		if(vis[v]||v==fa)continue;
		fr(v,u);
		siz[u]+=siz[v];
		mxsiz[u]=max(mxsiz[u],siz[v]);
	}
	mxsiz[u]=max(mxsiz[u],tot-siz[u]);
	if(mxsiz[u]<mxsiz[rt])rt=u;
}
LL dis[N];
vector<int>vec;
void getdis(int u,int fa){
	siz[u]=1;
	vec.emplace_back(u);
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v;LL w=e[i].w;
		if(v==fa||vis[v])continue;
		dis[v]=dis[u]+w;
		getdis(v,u);
		siz[u]+=siz[v];
	}
}
int stk[N],tp;
void solve(int u){
	vis[u]=1;vec.clear();
	dis[u]=0;getdis(u,0);
	sort(vec.begin(),vec.end());
	tp=0;
	for(int i:vec){
		while(tp&&dis[stk[tp]]>dis[i])tp--;
		if(tp)op[max(stk[tp],i)].emplace_back(min(stk[tp],i));
		stk[++tp]=i;
	}
	tp=0;
	for(int ID=vec.size()-1;ID>=0;ID--){
		int i=vec[ID];
		while(tp&&dis[stk[tp]]>dis[i])tp--;
		if(tp)op[max(stk[tp],i)].emplace_back(min(stk[tp],i));
		stk[++tp]=i;
	}
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v;
		if(vis[v])continue;
		rt=0;tot=siz[v];
		fr(v,u);solve(rt);
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<n;i++){
		int x,y,z;cin>>x>>y>>z;
		link(x,y,z);link(y,x,z); 
	}
	dfs0(1,0);dfs1(1);
	mxsiz[rt=0]=N;tot=n;
	fr(1,0);solve(rt);
	cin>>qn;
	for(int i=1;i<=qn;i++){
		int l,r;cin>>l>>r;
		q[r].emplace_back(make_pair(l,i));
	}
	T.init();
	for(int i=1;i<=n;i++){
		for(int v:op[i])
			T.chkmin(v,Dis(v,i));
		for(PII v:q[i]){
			LL res=T.que(v.first);
			ans[v.second]=(res==INF?-1:res);
		}
	}
	for(int i=1;i<=qn;i++)
		cout<<ans[i]<<'\n';
	return 0;
}

点分树 动态点分治

前置概念

树的直径\(dis(u,v)\) 为树上 \(u,v\) 两点的简单路径长度,找到 \(u,v\) 使得 \(dis(u,v)\) 取最大值。(其实跟这个没有关系)

树的重心:以该点为根时,最大子树的大小最小化。具体地,可以使用树形 DP 求解。

树上 \(K\) 级邻域查询:这是点分树解决的动态问题。具体来说,点有点权,支持动态修改,那么 对 \(u\) 的树上 \(K\) 级邻域查询就是所有和 \(u\) 距离不超过 \(K\) 的点的点权和。

P6329 【模板】点分树 | 震波

由于题目要求使用动态维护,想到用类似线段树的数据结构维护一下。那么这里的点分树是什么?

根据之前学习点分治的经验,我们会将原树不断地分成若干半,其中分割点为每棵树的重心。近似地把搜索过程看成分层结构,那么点分树就是指把这些相邻层的重心两两连边得到的树形结构。

点分治的重心分治结构使得点分树的深度为 \(O(\log n)\) 的。

点分树和原树没有必然联系,有且仅有一个特别好的性质:\(u,v\) 在点分树上的 LCA 一定在原树 \(u\rightarrow v\) 的路径上,即 \(dis(u,lca)+dis(lca,v)=dis(u,v)\),其中 \(dis(x,y)\) 表示 \(x,y\) 在原树上的最短路径长,\(lca\) 指两点在点分树上的 \(\text{LCA}\)

如上性质我们便可以联想到一种可能的方法:建出点分树后,对每个点 \(u\) 动态开点权值线段树,下标 \(i\) 记录 \(dis(u,z)=i\)\(\sum a_z\)

由于之前点分治的经历,我们察觉到一个点对答案做出贡献当且仅当:

  1. 其与上若干层祖先的其他子树中的点连线时。
  2. 其与下若干层儿子直接连线时。

每次更新时,把自己的信息上推到自己的所有祖先的动态开点线段树上,这样就解决了第一个需求,让其他子树的点可以直接与其匹配。

我们又发现,从祖先的角度看,可以获取自己所有儿子的信息,那不是就满足了第二个需求了吗?

于是乎粗略的思路就是如此。建两棵线段树,第一棵 T1 维护 \(x\) 在点分树子树内与 \(x\) 距离为 \(i\) 的有多少个,第二棵 T2 维护在 \(x\) 点分树子树内距离 \(fa_x\) 距离为 \(i\) 的有多少个,这里的 \(fa_x\) 指的也是点分树上的父亲。

那我们的查询过程只需要从节点 \(z\) 一直往上爬,对于节点 \(z\) 统计其 T1 的 \([0,k]\)(点分树子树内答案),然后子树外答案往上爬。每一层到节点 \(u\),上一层到的节点称为 \(pre\)。答案就加上 \(u\) T1 的 \([0,k-dis(u,z)]\) 部分,同时为了避免算到已经计算过的点权,减去 \(pre\) T2 的 \([0,k-dis(u,z)]\) 部分。

对于修改操作,我们也顺着节点 \(z\) 往上爬然后对应修改祖先线段树的值即可。实际实现中我们不需要真的把点分树建出来,只需要记录每个节点在点分树上的父亲即可。

时间复杂度由于线段树的存在是 \(O(n\log^2 n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+5,INF=1e9;
vector<int>G[N];
inline void ins(int x,int y){
	G[x].push_back(y);
}
int ans;
int n,m;
int f[N][21],dep[N];
inline void dfs0(int u,int fa){
	dep[u]=dep[fa]+1;
	for(int v:G[u]){
		if(v==fa)continue;
		f[v][0]=u;
		for(int j=1;(1<<j)<=dep[u];j++)
			f[v][j]=f[f[v][j-1]][j-1];
		dfs0(v,u);
	}
}
int rt,mx[N],siz[N];
bool vis[N];
inline void fr(int u,int fa,int S){
	siz[u]=1;
	mx[u]=0;
	for(int v:G[u]){
		if(v==fa||vis[v])continue;
		fr(v,u,S);
		siz[u]+=siz[v];
		mx[u]=max(mx[u],siz[v]);
	}
	mx[u]=max(mx[u],S-siz[u]);
	if(mx[rt]>mx[u])rt=u;
}
int dfa[N],val[N];
inline int LCA(int x,int y){
	if(dep[x]<dep[y])swap(x,y);
	for(int i=20;i>=0;i--){
		if(dep[f[x][i]]>=dep[y])
			x=f[x][i];
	}
	if(x==y)return x;
	for(int i=20;i>=0;i--){
		if(f[x][i]!=f[y][i])
			x=f[x][i],y=f[y][i];
	}
	return f[x][0];
}
inline int gtdis(int x,int y){
	return dep[x]+dep[y]-2*dep[LCA(x,y)];
}
inline void dvd(int u,int tot){
	vis[u]=1;
	for(int v:G[u]){
		if(vis[v])continue;
		rt=0;
		int sz=(siz[v]<siz[u]?siz[u]:(tot-siz[u]));
		fr(v,u,sz);
		dfa[rt]=u;//record father
		dvd(rt,sz);
	}
}
struct Sgt{
	#define ls (t[p].lc)
	#define rs (t[p].rc)
	#define mid ((l+r)>>1)
	struct Node{
		int lc,rc,sum;
	}t[N<<5];
	int rt[N];
	int ncnt;
	inline void pushup(int p){
		t[p].sum=t[ls].sum+t[rs].sum;
	}
	inline void update(int &p,int l,int r,int pos,int v){
		if(!p)p=++ncnt;
		if(l==r){t[p].sum+=v;return ;}
		if(pos<=mid)update(ls,l,mid,pos,v);
		else update(rs,mid+1,r,pos,v);
		pushup(p);
	}
	inline int query(int p,int l,int r,int L,int R){
		if(!p)return 0;
		if(L<=l&&r<=R)return t[p].sum;
		int res=0;
		if(L<=mid)res+=query(ls,l,mid,L,R);
		if(R>mid)res+=query(rs,mid+1,r,L,R);
		return res;
	}
	#undef ls
	#undef rs
	#undef mid
}T1,T2;
inline void modi(int pos,int v){
	int cur=pos;
	while(cur){
		T1.update(T1.rt[cur],0,n-1,gtdis(cur,pos),v);
		if(dfa[cur])T2.update(T2.rt[cur],0,n-1,gtdis(dfa[cur],pos),v);//例行维护
		cur=dfa[cur];
	}
}
inline int query(int pos,int k){
	int cur=pos,pre=0,res=0;
	while(cur){
		if(gtdis(cur,pos)>k){
			pre=cur,cur=dfa[cur];
			continue;//由于原树中两点距离关系可能与点分树中不同,不能break
		}
		res+=T1.query(T1.rt[cur],0,n-1,0,k-gtdis(cur,pos));
		if(pre)res-=T2.query(T2.rt[pre],0,n-1,0,k-gtdis(cur,pos));//文中说的贡献就在此处
		pre=cur,cur=dfa[cur];
	}
	return res;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>val[i];
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		ins(u,v);
		ins(v,u);
	}
	dfs0(1,0);
	mx[0]=INF;rt=0;
	fr(1,0,n);
	dvd(rt,n);
	for(int i=1;i<=n;i++)
		modi(i,val[i]);
	for(int i=1;i<=m;i++){
		int opt,x,y;
		cin>>opt>>x>>y;
		x^=ans,y^=ans;
		if(opt){
			modi(x,y-val[x]);
			val[x]=y;
		}
		else {
			ans=query(x,y);
			cout<<ans<<'\n';
		}
	}
	return 0;
}
posted @ 2025-04-24 14:51  TBSF_0207  阅读(36)  评论(0)    收藏  举报