【笔记分享】点分树及其应用
CDshishi_ldw · · 算法·理论
点分树
定义
点分树是也可以称为重心重构树,是基于原树重心重构。如下图。
点分树是在重心剖分过程中每一个找到的重心,将上一层剖分时的重心设为它的父亲 ,得到一颗大小不变、最多
性质
- 性质一:点分树上
x 子树对应原树上以x 为重心时连接的一个连通块。 - 性质二:点分树上的两个点
x,y 的lca(x,y) 一定在原树上两点的简单路径上。
用点分树解决问题主要是利用性质二。比如要统计与
点分树上的容斥。利用点分树统计信息时的一般过程是从下到上逐层统计。当离开当前子树向上时,需要容斥掉当前子树对答案的重复贡献,这个过程与点分治统计答案时的容斥相似。
具体通过几道题目来理解。
例题一:P6329【模板】点分树 / 震波
题意
一颗带点权树,有两种操作:修改
解析
如果不考虑修改,如何查询
再考虑修改。为了支持修改,我们将上面的数据结构换为线段树或者树状数组(支持单点修改、前缀和查询)。 首先,我们只考虑某一重心
接下来就是修改。还是考虑在当前子树
为了保证一致性与复杂度,我们按原树的重心重构得到点分树,在点分树上的每个点建立两颗线段树
点分树的构建
按上面的分析,我们在修改点
void divcent(int u){
vis[u]=1;
for(int v:g[u]){
if(vis[v])continue;
sum = siz[v];msiz[rt=0]=n;
getrt(v , u); //查找子树重心
f[rt]=u; //记录每层重心的父节点
divcent(rt);
}
}
计算树上的两点距离
可以在原树上预处理LCA,然后
修改点权
修改点权时,只需要在原树上修改点
void update(int x,int v){
int now = x;
while(now){
t1.update(t1.rt[now] , 1 , n, getdis(x,now)+1 , v);//t1上加权重
if(f[now])t2.update(t2.rt[now],1,n,getdis(x, f[now])+1 , v);
now=f[now];
}
}
查询 x 的答案
查询
int query(int x,int k){
int now=x, ret=0;
while(now){
if(getdis(x,now) <= k)//T1查询
ret+= t1.ask(t1.rt[now], 1,n, 1 , k-getdis(x,now)+1);
if(f[now] && getdis(x,f[now]) <= k)//T2容斥
ret-= t2.ask(t2.rt[now],1,n,1,k-getdis(x,f[now])+1);
now=f[now];
}
return ret;
}
总结算法
- 原树上预处理LCA。方便计算两点在原树上的距离
dis(x,y) 。 - 构建点分树。利用重心剖分构建重心重构树,记录每个重心的父节点
f[u] 。 - 点分树上每个点维护两颗线段树
T1,T2 。T1 维护x 为根子树内每个点dis(x,i) 为下标的点权和,T2 维护dis(fa[x],i) 为下标的点权和. - 处理每个查询与修改。
- 查询时,从
x 开始,在x.T1 中查询[1,k-dis(u,x)] 范围的和ans1 ,在x.T2 中查询[1,k-dis(fa[u],x)] 范围的和ans2 ,ans+=ans1-ans2 。然后在点分树上向上移动(x=f[x] )。 - 修改时,从
x 开始,在x.T1 中修改dis(u,x) 位置的点权+v ,在x.T2 中修改dis(fa[u],x) 位置的点权w_x 。然后在点分树上向上移动(x=f[x] )。
- 查询时,从
:::success[完整代码参考]
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+100;
const int M=5e6+100;
struct seg{
int rt[N];
int tot,lc[M],rc[M],s[M];
void pushup(int p){s[p]=s[lc[p]] + s[rc[p]]; }
void update(int& p,int l,int r,int k,int v){
if(!p)p=++tot;
if(l==r){s[p]+=v;return;}
int mid=(l+r)>>1;
if(k<=mid)update(lc[p],l,mid,k,v);
else update(rc[p],mid+1,r,k,v);
pushup(p);
}
int ask(int p,int l,int r,int ql,int qr){
if(!p)return 0;
if(ql<=l && r<=qr)return s[p];
int mid=(l+r)>>1;
int ret=0;
if(ql<=mid)ret+=ask(lc[p],l,mid,ql,qr);
if(mid<qr)ret+=ask(rc[p],mid+1,r,ql,qr);
return ret;
}
}t1,t2;
int n,m,rt,sum,val[N],msiz[N],siz[N],vis[N];
int dis[N],fa[N][20];
int f[N];//重构树父亲表示法
vector<int>g[N];
void dfs(int x,int ff){
fa[x][0]=ff;
for(int j=1;(1<<j)<=dis[x];j++)
fa[x][j]=fa[fa[x][j-1]][j-1];
for(auto v:g[x]){
if(v==ff)continue;
dis[v]=dis[x]+1;
dfs(v,x);
}
}
int lca(int x,int y){
if(dis[x]<dis[y])swap(x,y);
int k=dis[x]-dis[y];
for(int i=0;(1<<i)<=k;i++)
if(k&(1<<i))x=fa[x][i];
if(x==y)return x;
for(int i=19;i>=0;i--){
if(fa[x][i]!=fa[y][i])
x=fa[x][i],y=fa[y][i];
}
return fa[x][0];
}
int getdis(int x,int y){return dis[x]+dis[y]-2*dis[lca(x,y)];}
void getrt(int u,int ff){//cerr<<u<<endl;
siz[u]=1;msiz[u]=0;
for(int v:g[u]){
if(v==ff || vis[v]) continue;
getrt(v,u);
msiz[u]=max(msiz[u],siz[v]);
siz[u]+=siz[v];
}
msiz[u]=max(msiz[u],sum-siz[u]);
if(msiz[u] < msiz[rt]) rt=u;
}
void divcent(int u){
vis[u]=1;
for(int v:g[u]){
if(vis[v])continue;
sum = siz[v];msiz[rt=0]=n;
getrt(v , u);
f[rt]=u;
divcent(rt);
}
}
void update(int x,int v){
int now = x;
while(now){
t1.update(t1.rt[now] , 1 , n, getdis(x,now)+1 , v);//t1上加权重
if(f[now])t2.update(t2.rt[now],1,n,getdis(x, f[now])+1 , v);
now=f[now];
}
}
int ask(int x,int k){
int now=x, ret=0;
while(now){
if(getdis(x,now) <= k)
ret+= t1.ask(t1.rt[now], 1,n, 1 , k-getdis(x,now)+1);
if(f[now] && getdis(x,f[now]) <= k)
ret-= t2.ask(t2.rt[now],1,n,1,k-getdis(x,f[now])+1);
now=f[now];
}
return ret;
}
int main(){
//freopen("1.txt","r",stdin);
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;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1,0);//getlca
msiz[rt=0]=sum=n;
getrt(1,0);
divcent(rt);//点分树
for(int i=1;i<=n;i++)update(i,val[i]);
int lastans=0;
for(int i=1;i<=m;i++){
int op,x,y;
cin>>op>>x>>y;
x^=lastans;y^=lastans;
if(op==1){
update(x,y-val[x]);
val[x]=y;
}else{
lastans=ask(x,y);
cout<<lastans<<"\n";
}
}
return 0;
}
:::
小结:第一题我们学习了点分树解决问题的一般过程,了解了点分树的来源,下面我们就用点分树思想与性质来解决下面的题目。
P10603 BZOJ4372 烁烁的游戏
题意
给一棵
Q x:询问结点 M x d w:将树上与结点
解析
本题与上一题不同的地方在于区间修改,单点查询。
点分树是解决 树上连通块信息问题 的利器。与前面的套路相同,我们在点分树上每个点 建立数据结构维护信息。在 修改/查询时 从下到上 遍历点分树上的祖先并 修改/查询 这些点上的数据结构信息。通常每个点需要建立多个数据结构,用容斥原理来防止部分答案被重复计算。
本题,我们在点分树上每个点同样建立两颗线段树
查询时,从
修改时,从
查询时,从
修改与查询参考:
int ask(int x,int k){
int ret = t1.ask(t1.rt[x],0,n, 0);//自己的贡献
for (int now=x; f[now]; now=f[now]){
int dist = getdis(x,f[now]);
ret += t1.ask(t1.rt[f[now]], 0,n, dist );//加上父亲节点的T1 贡献
ret -= t2.ask(t2.rt[now], 0,n, dist); //容斥掉当前树的T2 贡献
}
return ret;
}
void update(int x,int k ,int v){
t1.update(t1.rt[now] , 0 , n, 0,k , v);//对自己子树的贡献范围[0,k]
for(int now=x; f[now]; now=f[now]){
int dist = getdis(x,f[now]);
if(dist > k)continue;
t1.update(t1.rt[f[now]] , 0 , n, 0,k-dist , v);//对父亲T1的贡献范围[0,k-dist]
t2.update(t2.rt[now] , 0 , n, 0,k-dist , v);//对当前树T2贡献范围[0,k-dist]
}
}
因为数据结构只需要支持区间修改单点查询,也可以通过差分变为单点修改,区间查询。通过动态树状数组来实现,具体参考下面详细代码: :::success[完整代码参考]
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
typedef long long ll;
struct BIT{//支持维护[0,n]范围,内部向右偏移一位
vector<int>c; int siz;
void init(int n){c.resize(n+2);siz=n+1;}
void add(int k, int d) {
k++;
for (; k <=siz; k += (k & -k))
c[k] += d;
}
int query(int k){
int ret = 0;
k++;
for(; k>0 ; k -= (k&-k))
ret += c[k];
return ret;
}
}t1[N],t2[N];
int n,m,rt,sum,val[N],msiz[N],siz[N],vis[N];
int f[N];//重构树父亲表示法
vector<int>g[N];
namespace LCA{
int dis[N],fa[N][20];
void dfs(int x,int ff){
fa[x][0]=ff;
for(int j=1;(1<<j)<=dis[x];j++)
fa[x][j]=fa[fa[x][j-1]][j-1];
for(auto v:g[x]){
if(v==ff)continue;
dis[v]=dis[x]+1;
dfs(v,x);
}
}
int lca(int x,int y){
if(dis[x]<dis[y])swap(x,y);
int k=dis[x]-dis[y];
for(int i=0;(1<<i)<=k;i++)
if(k&(1<<i))x=fa[x][i];
if(x==y)return x;
for(int i=19;i>=0;i--){
if(fa[x][i]!=fa[y][i])
x=fa[x][i],y=fa[y][i];
}
return fa[x][0];
}
int getdis(int x,int y){return dis[x]+dis[y]-2*dis[lca(x,y)];}
}
void getrt(int u,int ff){
siz[u]=1;msiz[u]=0;
for(int v:g[u]){
if(v==ff || vis[v]) continue;
getrt(v,u);
msiz[u]=max(msiz[u],siz[v]);
siz[u]+=siz[v];
}
msiz[u]=max(msiz[u],sum-siz[u]);
if(msiz[u] < msiz[rt]) rt=u;
}
void divcent(int u){
vis[u]=1;
for(int v:g[u]){
if(vis[v])continue;
sum = siz[v];msiz[rt=0]=n;
getrt(v , u);
f[rt]=u;
t2[rt].init(siz[u]);
divcent(rt);
}
t1[u].init(siz[u]);
}
void update(int u,int k,int v){
t1[u].add(0,v);t1[u].add(k+1,-v);
for(int now=u ; f[now] ; now=f[now]){
int d = LCA::getdis(u,f[now]);
if(k < d)continue;
t1[f[now]].add(0,v);t1[f[now]].add(k-d+1,-v);
t2[now].add(0,v);t2[now].add(k-d+1 , -v);
}
}
int query(int u){
int ret=t1[u].query(0);
for(int now=u ; f[now] ; now=f[now]){
int d = LCA::getdis(u,f[now]);
ret += t1[f[now]].query(d);
ret -= t2[now].query(d);
}
return ret;
}
int main(){
cin>>n>>m;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
g[u].push_back(v); g[v].push_back(u);
}
LCA::dfs(1,0);
msiz[rt=0]=sum=n;
getrt(1,0);
divcent(rt);//点分树,并为每个点初始化2颗树状数组
for(int i=1;i<=m;i++){
char p;int x,k,v;
cin>>p>>x;
if(p=='Q'){
cout<<query(x)<<"\n";
}else{
cin>>k>>v;
update(x,k,v);
}
}
return 0;
}
:::
例题三:P3345 [ZJOI2015] 幻想乡战略游戏
题意
有一颗
解析
换根的思路。本题是找最佳决策点问题,一个暴力的做法,枚举每个点作为决策点,计算
设
则从
总的变化为:
解析
因为
点分治擅长通过分治的方式找到
对
如下示意图:
然后再查询序列
- 预处理:
- 预处理
T2 原树的 lca,方便计算任意两点的距离。 - 构建
T2 的点分树。点分树上维护 一个 最小值与次小值。
- 预处理
- 对
T1 进行点分治。设当前点为u 。- 收集
u 子树中所有点到u 的距离dist1[j] ,存入序列C 中。 - 从序列
C 取出每个点j , 维护j 在T2 的点分树上到每个祖先的距离值dist2[j] 加上dist1[j] 的最小值与次小值。 - 查询:取出
C 中每个点x ,查询x 的D(i) . 在T2 的点分树上的每个祖先查询除x 外的\min(dist1[i]+dist2[i]) ,然后加上dist1[x]+dist2[x] 即可。
- 收集
练习
P10604/BZOJ4317 Atm 的树
P3241 [HNOI2015] 开店