动态点分治
动态点分治
\(update\):\(2020.10.20\)发现以前对动态点分治的理解是错误的,进行更正。
由于蒟蒻太逊,现在才开始学动态点分治,写一个\(blog\)吧。
动态点分治是利用点分治的过程,建成一颗由子树重心连接而成的点分树,这棵树的高度为\(\log n\)级别的,因此可以通过暴力跳父亲完成修改操作。
建立点分树的过程,就是按照点分治的流程,记录上级重心并连接,即可获得一棵点分树。
点分树的结构与原树不相同,但是由于它具有优秀的树高,同时可以发现,对于任意一个节点,从该节点到点分树根的路径具有容斥的性质(可以把点分树根节点当做包含所有点信息的圆,那么它的子节点就是一个更小的圆,任何子节点的圆都包含在父节点内)。通过这一性质,我们可以通过从小范围开始,逐步扩大范围,同时容斥,就可以求出关于一个节点的信息了。
其他内容和细节,还是放到例题中去吧。
Luogu6329 【模板】点分树 | 震波
注:以下内容中,不经特别说明的树均代表点分树。
对于每个节点,维护其子树中所有节点到达它的距离之和,可以利用两个树状数组,为了节约空间开销,我们把每个节点的两个树状数组大小分别定为它的子树的最大深度以及它的子树到达其父亲的最大距离(懒得计算也可以直接用子树大小代替,因为其父节点一定与子树中一个节点相邻,所以第二个树状数组不用多开,这一点下面也会提到。不过不建议这样做,毕竟空间开销大了),这样的空间复杂度上限为\(n \log n\)。
计算时,用父节点子树全部满足题意的值减去子节点子树对父节点的贡献(注:不满足条件即贡献为\(0\))。
对于一个节点\(x\),记录\(x\)子树对\(x\)的贡献为\(S1_x\),\(x\)子树对\(fa_x\)的贡献为\(S2_x\)。
虽然点分树中的父子关系与原树不同,但这样的计算基于的基础在于,每个节点的贡献都会被算到且仅被算过一次。同时我们需要保证,计算贡献时我们所用的任意点到询问点距离为它们之间的最短距离。
假设我们需要求到达\(x_0\)节点距离不超过\(k\)的所有点的权值,设\(x_0\)到根节点的路径为\(\{x_0,x_1,\cdots,x_n\}\)。
先证明每个节点的贡献都会被算到,且计算贡献时我们所用的任意点到询问点距离为它们之间的最短距离。
对于任意节点\(y\):
我们考虑建立点分树的过程,对于每个节点,它都记录了与其相邻的原树中的一个连通块所有节点的信息,设节点\(u\)记录的连通块为\(T_u\)。
贡献一定会被统计,因为\(T_{x_n}=V\)(点集)。
如果\(y\)在\(T_{x_0}\)中,那么 在\(x_0\)中会计算\(y\)的贡献,显然此时计算的距离为\(x_0 \rightarrow y\)的最短路径。
如果\(y\)不在\(T_{x_0}\)中,由于\(T_{x_0} \subseteq T_{x_1} \subseteq \cdots \subseteq T_{x_n}\),我们可以证明,包含\(y\)的最小连通块\(T_{x_i}\)会记录\(x_0\)与\(y\)之间的最短距离。因为树上最短距离只需要保证不走重复边就可以了,那么我们考虑\(x_i\)处,\(x_0\)与\(y\)一定分别属于\(x_i\)的不同子树,这里的子树指的不是原树上的子树,而是建立点分树时的不同连通块(根据建立点分树的过程,如果\(x_0\)与\(y\)属于\(x_i\)的同一子树,那么显然包含\(y\)的最小连通块不是\(T_{x_i}\))。既然是不同子树,那么\(x_0\)到\(y\)的最短距离一定是\(x_0 \rightarrow x_i \rightarrow y\)(可以把\(x_i\)当成建点分树时的\(LCA\)来理解)。
所以,在\(x_i\)处,一定会计算\(y\)的贡献。
证明完毕!
继续,证明每个节点的贡献仅被算过一次。
对于任意有贡献的节点\(y\):
如果\(y\)离\(x_0\)的距离很近,那么\(y\)重复走一些路径,可能使得总距离仍然\(\le k\),但是这明显是不符合题意的。
同样设包含\(y\)的最小连通块为\(T_{x_i}\),那么这次的贡献会统计。
如果在一个比\(T_{x_i}\)大的集合\(T_{x_j}\)中,\(y\)重复走一些路径,使得总距离仍然\(\le k\),那么\(y\)必然也出现在\(T_{x_{j-1}}\)中,根据这一性质,我们进行容斥,每次计算\(T_{x_k}(k \ne 0)\)的答案时,把属于\(T_{x_{k-1}}\)中的点在\(T_{x_k}\)的贡献去掉,就避免了重复统计。
最终\(y\)的贡献只计算了一次。
证明完毕!
所以,动态点分治的计算方式是正确的。
细节:
\(1.\)由于树状数组不易维护距离为\(0\)的信息(当然要强行维护也行),可以把我们要查询的节点单独加入答案。也就是\(x\)的权值不加入树状数组\(S1_x\)中,但是必须加入\(S2_x\)中。
\(2.\)选择\(ST\)表求\(LCA\)是本题的最佳选择,因为它能够\(O(1)\)计算\(LCA\),能够支撑大量两点间距离信息的访问。
总结:点分树利用了巧妙的结构,使得询问节点到根节点的路径必然形成大集合包含小集合的关系,从而不重不漏地计算了所有答案。同时,由于点分树是特殊构造的树,学习时必须完全区别开点分树和原树(与\(LCT\)相似),才能避免概念混淆。
\(Code:\)
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
#define N 100005
#define INF 1000000007
using namespace std;
int n,m,x,y,opt,lsz,mx1,mx2,val[N];
int tot,fr[N],nxt[N << 1],d[N << 1];
int rtsz,rt,sz[N],f[N],dep[N];
int ans=0,cnt,bg[N],lg2[N << 1],dfn[N << 1],st[N << 1][22];
bool vis[N];
struct BIT
{
#define lowbit(x) (x & (-x))
vector<int>c;
int n;
void setn(int o)
{
for (int i=0;i<=o+2;i++)
c.push_back(0);
n=o;
}
int getsum(int x)
{
x=min(x,n);
int ans=0;
while (x)
{
ans+=c[x];
x-=lowbit(x);
}
return ans;
}
void update(int x,int y)
{
while (x<=n)
{
c[x]+=y;
x+=lowbit(x);
}
}
}S1[N],S2[N];
void add(int x,int y)
{
tot++;
d[tot]=y;
nxt[tot]=fr[x];
fr[x]=tot;
}
void dfs(int u,int F)
{
dfn[++cnt]=u;
bg[u]=cnt;
for (int i=fr[u];i;i=nxt[i])
{
int v=d[i];
if (v==F)
continue;
dep[v]=dep[u]+1;
dfs(v,u);
dfn[++cnt]=u;
}
}
int lca(int u,int v)
{
u=bg[u],v=bg[v];
if (u>v)
swap(u,v);
int k=lg2[v-u+1];
return (dep[st[u][k]]<dep[st[v-(1 << k)+1][k]])?st[u][k]:st[v-(1 << k)+1][k];
}
int dis(int u,int v)
{
return dep[u]+dep[v]-(dep[lca(u,v)] << 1);
}
void findrt(int u,int F,int rn)
{
int mx=-1;
sz[u]=1;
for (int i=fr[u];i;i=nxt[i])
{
int v=d[i];
if (v==F || vis[v])
continue;
findrt(v,u,rn);
sz[u]+=sz[v];
if (sz[v]>mx)
mx=sz[v];
}
mx=max(mx,rn-sz[u]);
if (mx<rtsz)
rtsz=mx,rt=u;
}
void getrt(int u,int rn)
{
rtsz=INF;
findrt(u,0,rn);
}
void calc_dis(int u,int F,int rt,int frt)
{
mx1=max(mx1,dis(u,rt));
if (frt)
mx2=max(mx2,dis(u,frt));
for (int i=fr[u];i;i=nxt[i])
{
int v=d[i];
if (v==F || vis[v])
continue;
calc_dis(v,u,rt,frt);
}
}
void update_dis(int u,int F,int rt,int frt)
{
if (u!=rt)
S1[rt].update(dis(u,rt),val[u]);
if (frt)
S2[rt].update(dis(u,frt),val[u]);
for (int i=fr[u];i;i=nxt[i])
{
int v=d[i];
if (v==F || vis[v])
continue;
update_dis(v,u,rt,frt);
}
}
void solve(int u)
{
int tsz=lsz;
mx1=0,mx2=0;
calc_dis(u,0,u,f[u]);
S1[u].setn(mx1);
S2[u].setn(mx2);
update_dis(u,0,u,f[u]);
vis[u]=true;
for (int i=fr[u];i;i=nxt[i])
{
int v=d[i];
if (vis[v])
continue;
lsz=(sz[v]<sz[u])?sz[v]:tsz-sz[u];
getrt(v,lsz);
f[rt]=u;
solve(rt);
}
}
void calc(int x,int k)
{
int u=x;
ans=val[u]+S1[u].getsum(k);
while (f[u])
{
int len=dis(f[u],x);
if (len<=k)
ans+=val[f[u]],ans+=S1[f[u]].getsum(k-len),ans-=S2[u].getsum(k-len);
u=f[u];
}
}
void update(int x,int y)
{
int u=x,t=y-val[x];
val[x]=y;
while (f[u])
{
S1[f[u]].update(dis(f[u],x),t);
S2[u].update(dis(f[u],x),t);
u=f[u];
}
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++)
scanf("%d",&val[i]);
for (int i=1;i<n;i++)
{
scanf("%d%d",&x,&y);
add(x,y),add(y,x);
}
dep[1]=1;
dfs(1,0);
lg2[0]=-1;
for (int i=1;i<=cnt;i++)
st[i][0]=dfn[i],lg2[i]=lg2[i >> 1]+1;
for (int j=1;j<=lg2[cnt];j++)
for (int i=1;i<=cnt-(1 << j)+1;i++)
if (dep[st[i][j-1]]<dep[st[i+(1 << j-1)][j-1]])
st[i][j]=st[i][j-1]; else
st[i][j]=st[i+(1 << j-1)][j-1];
getrt(1,n);
lsz=n;
solve(rt);
while (m--)
{
scanf("%d%d%d",&opt,&x,&y);
x^=ans,y^=ans;
if (opt==0)
calc(x,y),printf("%d\n",ans); else
update(x,y);
}
return 0;
}