====== 点分树 ======
===== 算法简介 =====
点分治的动态版本,可以动态维护树上路径信息,一般会与线段树或平衡树等数据结构配合使用。
特别适用于一些需要维护与路径长度相关信息的题目。
一般空间复杂度为 $O(n\log n)$,单次查询或修改时间复杂度为 $O\left(\log^2 n\right)$,**常数极大,慎用。**
===== 算法思想 =====
把点分治过程中得到的重心连接,得到一棵虚树,该树有如下性质:
1、深度不超过 $\log n$。
2、所有结点的子树大小和不超过 $n\log n$。
由于点分治最多递归 $\log n$ 层,所以性质一成立。
对性质二,考虑每个结点对子树大小和的贡献。
每个结点对每个祖先结点(包括它自己)产生一个贡献,而由性质一,每个节点最多有 $\log n$ 个祖先结点,所以每个结点贡献最多为 $\log n$。
所以有所有结点的子树大小和不超过 $n\log n$,性质二证毕。
有了这两个性质的保障,便可以用点分树暴力地解一些题目。
一般思路为对每个结点,维护该结点在虚树中的子树信息。
对每个结点,若使用线段树或平衡树等数据结构维护子树信息,根据性质二,空间复杂度仅为 $n\log n$。
修改和查询都暴力跳 $\text{fa}$ ,若每次查询线段树或平衡树数据结构维护子树信息,根据性质一,有单次查询或修改时间复杂度为 $O\left(\log^2 n\right)$。
===== 算法习题 =====
=== 习题一 ===
[[https://www.luogu.com.cn/problem/P6329|洛谷p6329]]
**题意**
给出一棵 $n$ 个结点的点权树,相邻点距离为 $1$ ,接下来 $m$ 个操作,操作有以下两种:
操作0:输出所有到结点 $x$ 距离不超过 $k$ 的结点的点权和。
操作1:将结点 $x$ 点权修改为 $y$。
同时算法要求强制在线,对每次操作的参数 $x、y、k$,都需要异或上一次输出的答案。
**题解**
考虑对每个结点,建立两个树状数组,第一个树状数组维护所有在虚树中属于结点 $x$ 的子树且到 $x$ 距离等于 $k$ 的点权和的数组。
第二个树状数组维护所有在虚树中属于结点 $x$ 的子树且到结点 $x$ 在虚树中的父亲的距离等于 $k$ 的点权和的数组。
记 $\text{dist}(x,y)$ 为结点 $x$ 到 $y$ 距离,可以用树剖或者欧拉序列加 $\text{ST}$ 表求。
对查询操作,可先查询 $x$ 第一个树状数组中距离小于等于 $k$ 的点权和。然后开始暴力跳 $\text{fa}$。
每次加上 $\text{fa}$ 结点第一个树状数组中距离小于等于 $k-\text{dist}(x,fa)$ 的点权和。
但是这样会重复计算 $\text{fa}$ 结点的子结点的子树贡献。
所以再减去 $\text{fa}$ 结点的子结点的第二个树状数组中距离小于等于 $k-\text{dist}(x,fa)$ 的点权和。
最后便可以得到答案,时间复杂度为 $O\left(\log^2 n\right)$。
对于修改操作,需要同时维护每个结点的第一个和第二个树状数组。
同样先修改 $x$ 第一个树状数组中距离为 $0$ 的点权和(因为 $x$ 到自身距离为 $0$ )。然后开始暴力跳 $\text{fa}$。
每次修改 $\text{fa}$ 结点第一个树状数组中距离为 $\text{dist}(x,fa)$ 的点权和。
再修改 $\text{fa}$ 结点的子结点的第二个树状数组中距离等于 $\text{dist}(x,fa)$ 的点权和。
最后便可以完成修改,时间复杂度也是 $O\left(\log^2 n\right)$。
const int MAXN=1e5+5,MAXM=20,inf=1e6+5;
struct Edge{
int to,next;
}edge[MAXN<<1];
int n,q,edge_cnt,head[MAXN];
inline void Insert(int u,int v){
edge[++edge_cnt].to=v;
edge[edge_cnt].next=head[u];
head[u]=edge_cnt;
}
int a[MAXN],sz[MAXN],dep[MAXN],mson[MAXN],f[MAXN],tot_sz,root,root_sz;
bool vis[MAXN];
struct LCA{
int B[MAXN<<1],F[MAXN<<1],pos[MAXN],n;
int d[MAXN<<1][MAXM],lg2[MAXN<<1];
inline void push(int index,int depth){
F[n]=index;
B[n]=depth;
n++;
}
void dfs(int u,int fa,int depth){
pos[u]=n;dep[u]=depth;
push(u,depth);
for(int i=head[u];i;i=edge[i].next){
int v=edge[i].to;
if(v==fa)
continue;
dfs(v,u,depth+1);
push(u,depth);
}
}
void build(int root){
dfs(root,-1,0);
lg2[1]=0;
_rep(i,2,n)
lg2[i]=lg2[i>>1]+1;
_for(i,0,n)
d[i][0]=i;
for(int j=1;(1<rig)
swap(lef,rig);
if(B[d[lef][lg2[len]]]c;
void build(int n){
this->n=n;
c.resize(n+1);
}
void add(int pos,int v){
++pos;
while(pos<=n){
c[pos]+=v;
pos+=lowbit(pos);
}
}
int query(int pos){
pos=min(pos+1,n);
int ans=0;
while(pos){
ans+=c[pos];
pos-=lowbit(pos);
}
return ans;
}
}tree_1[MAXN],tree_2[MAXN];
void find_root(int u,int fa){
sz[u]=1;mson[u]=0;
for(int i=head[u];i;i=edge[i].next){
int v=edge[i].to;
if(vis[v]||v==fa)
continue;
find_root(v,u);
sz[u]+=sz[v];
mson[u]=max(mson[u],sz[v]);
}
mson[u]=max(mson[u],tot_sz-sz[u]);
if(mson[u]>1)+1);tree_2[u].build(cur_sz+1);
for(int i=head[u];i;i=edge[i].next){
int v=edge[i].to;
if(vis[v])
continue;
tot_sz=sz[v]>sz[u]?cur_sz-sz[u]:sz[v];root_sz=MAXN;
find_root(v,u);
build_tree(root,u);
}
}
void update(int u,int val){
tree_1[u].add(0,val);
for(int i=u;f[i];i=f[i]){
int d=get_dis(u,f[i]);
tree_1[f[i]].add(d,val);
tree_2[i].add(d,val);
}
}
int query(int u,int k){
int ans=tree_1[u].query(k);
for(int i=u;f[i];i=f[i]){
int d=get_dis(u,f[i]);
if(d>k)
continue;
ans+=tree_1[f[i]].query(k-d);
ans-=tree_2[i].query(k-d);
}
return ans;
}
int main()
{
n=read_int(),q=read_int();
_rep(i,1,n)
a[i]=read_int();
int u,v;
_for(i,1,n){
u=read_int(),v=read_int();
Insert(u,v);
Insert(v,u);
}
lca.build(1);
root_sz=MAXN;
tot_sz=n;
find_root(1,0);
build_tree(root,0);
_rep(i,1,n)
update(i,a[i]);
int last=0,opt,x,y;
while(q--){
opt=read_int(),x=read_int()^last,y=read_int()^last;
if(opt==0)
enter(last=query(x,y));
else{
update(x,y-a[x]);
a[x]=y;
}
}
return 0;
}
=== 习题二 ===
[[https://www.luogu.com.cn/problem/P2056|洛谷p2056]]
**题意**
给出一棵 $n$ 个结点的树,相邻点距离为 $1$ ,一开始所有点都为黑点,接下来 $m$ 个操作,操作有以下两种:
操作1:改变结点 $x$ 的颜色(黑色变白色,白色变黑色)。
操作2:询问树上距离最远的黑色点对的距离,如果只有一个黑点输出 $0$ ,如果没有黑点输出 $-1$。
**题解1**
考虑对每个结点 $x$ ,建立两个大根堆,第一个堆维护每棵在虚树中属于结点 $x$ 的子树到结点 $x$ 的最大距离。
第二个堆维护所有在虚树中属于结点 $x$ 的子树的结点到 $x$ 在虚树中的父亲结点的距离。
最后建立一个答案堆,维护每个结点的答案,其中每个结点的答案为每个结点第一个堆的最大值和次大值和。
对于操作1,同样是暴力跳 $\text{fa}$ ,沿途维护结点的第一个堆和第二个堆。
关于堆的删除操作,可以考虑建两个堆,如果第一个堆存所有的数,第二个堆存要删除的数。
每次取第一个堆最大元素前先检测两个堆顶元素是否相同,相同则同时 $\text{pop()}$。
另外,求两个结点的距离好像可以通过 $bfs$ 预处理完成,可以避开欧拉序列加 $\text{ST}$ 表常数过大的问题。
记 $\text{dist}(x,y)$ 为结点 $x$ 到 $y$ 距离,可以用树剖或者欧拉序列加 ST 表求。
对于操作2,直接查询答案堆最大元素即可。
具体一些细节见代码。
const int MAXN=1e5+5,MAXM=20,inf=1e6+5;
struct Edge{
int to,next;
}edge[MAXN<<1];
int n,q,edge_cnt,head[MAXN];
inline void Insert(int u,int v){
edge[++edge_cnt].to=v;
edge[edge_cnt].next=head[u];
head[u]=edge_cnt;
}
int sz[MAXN],mson[MAXN],f[MAXN],tot_sz,root,root_sz;
int dep[MAXN],fdis[MAXM][MAXN],ans[MAXN];
bool vis[MAXN],turn_off[MAXN];
struct Heap{
priority_queue q1,q2;
void Insert(int x){
q1.push(x);
}
void Delate(int x){
if(q1.top()==x)
q1.pop();
else
q2.push(x);
}
int get_first(){
while((!q1.empty())&&(!q2.empty())&&(q1.top()==q2.top()))
q1.pop(),q2.pop();
if(q1.empty())
return -inf;
else
return q1.top();
}
int get_pair(){
if(q1.size()-q2.size()<2)
return -inf;
int t1=get_first(),t2;
q1.pop();
t2=get_first();
q1.push(t1);
return t1+t2;
}
}Ans,heap_1[MAXN],heap_2[MAXN];
void add_ans(int x){
ans[x]=heap_1[x].get_pair();
if(ans[x]!=-inf)
Ans.Insert(ans[x]);
}
void delate_ans(int x){
if(ans[x]!=-inf)
Ans.Delate(ans[x]);
}
void bfs(int s){
queueq;
q.push(s);
fdis[++dep[s]][s]=0;
while(!q.empty()){
int u=q.front();q.pop();
heap_2[s].Insert(fdis[dep[s]-1][u]);
for(int i=head[u];i;i=edge[i].next){
int v=edge[i].to;
if(vis[v])
continue;
if(fdis[dep[s]][u]+1sz[u]?cur_sz-sz[u]:sz[v];root_sz=MAXN;
find_root(v,u);
build_tree(root,u);
}
heap_1[u].Insert(0);
ans[u]=heap_1[u].get_pair();
if(ans[u]!=-inf)
Ans.Insert(ans[u]);
}
void Add(int u){
n++;
delate_ans(u);
heap_1[u].Insert(0);
add_ans(u);
for(int i=u;f[i];i=f[i]){
int d=fdis[dep[i]-1][u];
delate_ans(f[i]);
if(heap_2[i].get_first()!=-inf)
heap_1[f[i]].Delate(heap_2[i].get_first());
heap_2[i].Insert(d);
heap_1[f[i]].Insert(heap_2[i].get_first());
add_ans(f[i]);
}
}
void Delate(int u){
n--;
delate_ans(u);
heap_1[u].Delate(0);
add_ans(u);
for(int i=u;f[i];i=f[i]){
int d=fdis[dep[i]-1][u];
delate_ans(f[i]);
heap_1[f[i]].Delate(heap_2[i].get_first());
heap_2[i].Delate(d);
if(heap_2[i].get_first()!=-inf)
heap_1[f[i]].Insert(heap_2[i].get_first());
add_ans(f[i]);
}
}
int query(){
if(n==0)
return -1;
else if(n==1)
return 0;
else
return Ans.get_first();
}
int main()
{
n=read_int();
int u,v;
_for(i,1,n){
u=read_int(),v=read_int();
Insert(u,v);
Insert(v,u);
}
mem(fdis,127/3);
root_sz=MAXN;
tot_sz=n;
find_root(1,0);
build_tree(root,0);
q=read_int();
char opt;
int x;
while(q--){
opt=get_char();
if(opt=='G')
enter(query());
else{
x=read_int();
turn_off[x]^=1;
if(turn_off[x])
Delate(x);
else
Add(x);
}
}
return 0;
}
**题解2**
这个解法和点分树没什么关系,用的是括号序列加线段树,时空间复杂度都比点分树少一个 $\log$。
这里仅提供代码,有兴趣的可以自行学习。
const int MAXN=1e5+5,MAXN_2=3e5+5,inf=1e6+5;
struct Edge{
int to,next;
}edge[MAXN<<1];
int n,q,edge_cnt,head[MAXN],p[MAXN],a[MAXN_2],dfs_t;
inline void Insert(int u,int v){
edge[++edge_cnt].to=v;
edge[edge_cnt].next=head[u];
head[u]=edge_cnt;
}
struct Node{
int l,r;
int lc,rc,ans,l1,l2,r1,r2;
void build(int x){
if(x==1)
lc=rc=l1=l2=r1=r2=ans=0;
else{
if(!x)
lc=rc=0;
else if(x==-1)
lc=1;
else
rc=1;
ans=l1=l2=r1=r2=-inf;
}
}
}node[MAXN_2<<2];
inline void push_up(int k){
int l=k<<1,r=k<<1|1;
node[k].lc=node[l].lc+max(node[r].lc-node[l].rc,0);
node[k].rc=node[r].rc+max(node[l].rc-node[r].lc,0);
node[k].ans=max(max(node[l].ans,node[r].ans),max(node[l].r1+node[r].l2,node[l].r2+node[r].l1));
node[k].l1=max(node[l].l1,max(node[l].lc-node[l].rc+node[r].l1,node[l].lc+node[l].rc+node[r].l2));
node[k].l2=max(node[l].l2,node[l].rc-node[l].lc+node[r].l2);
node[k].r1=max(node[r].r1,max(node[l].r1+node[r].rc-node[r].lc,node[l].r2+node[r].lc+node[r].rc));
node[k].r2=max(node[r].r2,node[l].r2+node[r].lc-node[r].rc);
}
void build(int k,int lef,int rig){
node[k].l=lef,node[k].r=rig;
int mid=lef+rig>>1;
if(lef==rig){
node[k].build(a[mid]);
return;
}
build(k<<1,lef,mid);
build(k<<1|1,mid+1,rig);
push_up(k);
}
void update(int k,int pos){
if(node[k].l==node[k].r){
node[k].build(a[pos]);
return;
}
int mid=node[k].l+node[k].r>>1;
if(mid1)
enter(node[1].ans);
else if(n==1)
puts("0");
else
puts("-1");
}
else{
x=read_int();
if(a[p[x]])
n--;
else
n++;
a[p[x]]^=1;
update(1,p[x]);
}
}
return 0;
}