全局平衡二叉树
本文中的「重子节点」、「轻边」等术语定义同重链剖分。
全局平衡二叉树,即在重链剖分的基础之上,使用静态二叉树来维护重链,从而得到的常数优秀、复杂度 \(\mathcal O(\log n)\) 的数据结构。
全局平衡二叉树可以用于在 \(\mathcal O(\log n)\) 的时间内处理树上链修改/查询,可以代替树剖维护链上信息。(其实也可以维护子树信息,但是我不会。)
全局平衡二叉树
给定一棵以 \(0\) 为根的树,记 \(\textit{dep}_x\) 为 \(x\) 的深度。
给定 \(m\) 次询问,每次询问给出 \(l,r,z\),求 \(\sum\limits_{i=l}^r\textit{dep}_{\operatorname{lca}(i,z)}\bmod 201314\)。
为了便于分析,不妨令根节点为 \(1\)。
考虑这样一个事情,\(\textit{dep}_{\operatorname{lca}(i,z)}\) 其实就是 \(1\sim i\) 和 \(1\sim z\) 两条链上的公共点数。
询问可以对于每一个合法的 \(i\),将 \(1\sim i\) 链上的每一个点点权增加 \(1\),查询 \(1\sim z\) 链上的点数。
可以考虑将询问离线。容易发现,\([l,r]\) 的答案就是 \([1,r]\) 的答案减去 \([1,l-1]\) 的答案。\(1\sim n\) 依次加点,计算对应贡献即可。
这样就可以优化到 \(n\) 次修改,\(2m\) 次查询。
我们需要维护一个树上数据结构,能够高效地支持:
- 链上信息修改,即链上点权增加。
- 链上信息查询,即链上点权和。
全局平衡二叉树可以 \(\mathcal O(\log n)\) 实现这些操作。
主要性质
先看一个例子。
使用实线表示「重边」,虚线表示「轻边」。
例如,有这样一棵树:
其全局平衡二叉树为:
-
全局平衡二叉树是由若干棵静态二叉树组成的树,这些二叉树由内向轻边相连。
-
全局平衡二叉树的每一棵二叉树都维护一条重链的信息。其中序遍历是原重链按照深度单调递增得到的序列。
-
全局平衡二叉树的边分轻边和重边。重边是正常的边,也是原重链中的重边;轻边均为单向边,均从一棵二叉树的根节点指向其父节点(即「内向」)。(注意,「重子节点」和「轻子节点」一般情况下还是取重链剖分意义下的对应节点。)
-
全局平衡二叉树的树高为 \(\mathcal O(\log n)\)。
注意到,\(1\sim x\) 的路径上,如果有 \(a\) 条轻边,则代表至少有 \(a\) 条重链,而根据重链剖分的性质,重链数量为 \(\mathcal O(\log n)\),因此 \(\mathcal O(a)=\mathcal O(\log n)\)。
注意到,\(1\sim x\) 的路径上,如果有 \(b\) 条重边,因为全局平衡二叉树的建树方式(见下文),重边至多为 \(\mathcal O(\log n)\) 条。
建树
首先可以跟重链剖分一样,先 DFS 一遍预处理出原树上节点 \(x\) 的子树大小 \(\textit{size}_x\),重子节点 \(\textit{son}_x\),父节点 \(\textit{father}_x\)。在代码中,这一部分存储在名为 tree 的 namespace 中。
从根节点 \(1\) 开始,对于当前节点 \(x\) 所在的重链上所有节点的轻子节点递归建树。
递归结束后,便是对当前节点 \(x\) 所在重链部分(不一定完整)建树。
设 \(y_1,y_2,\cdots,y_k\) 是当前重链上按照深度从小到大排序后的节点。
对于重链上的节点 \(y_i\),可以计算 \(y_i\) 的权值:
特别地,\(s_0=0\)。
不妨设当前子树由 \(y_l\sim y_r\) 构成。
那么,若 \(i\) 满足:
则,\(i\) 就是当前部分的根节点,递归建左子树 \(y_l\sim y_{i-1}\),和右子树 \(y_{i+1}\sim y_r\) 即可。
同时,可以预处理出全局平衡二叉树上节点 \(x\) 的子树大小 \(\textit{size}'_x\)。
建树方式的好处
注意到对于 $x$,因为上文中 $x$ 是加权中点,因此左右子树的大小量级是相同的。
跳一次重边,节点的轻子树大小和至少会翻倍,于是跳的总重边数量不超过 $\mathcal O(\log n)$。
因此可以保证整个全局平衡二叉树的高度为 $\mathcal O(\log n)$。
参考代码
struct node{
int father,lChild,rChild;
}t[N+1];
//返回根节点
int buildBST(int l,int r,int point[],int s[]){
if(l==r){
return point[l];
}
if(r<l){
return 0;
}
int p=lower_bound(s+l,s+r+1,s[r]+s[l-1]>>1)-s+1;
p=point[p];
t[p].lChild=buildBST(l,p-1,point,s);
t[t[p].lChild].father=p;
t[p].rChild=buildBST(p+1,r,point,s);
t[t[p].rChild].father=p;
return p;
}
int build(int x){
int y=x;
do{
for(int i:tree::g[y]){
if(i==tree::son[y]||i==tree::father[y]){
continue;
}
t[build(i)].father=y;
}
y=tree::son[y];
}while(y);
static int point[N+1],s[N+1];
int size=0;
for(;x;x=tree::son[x]){
size++;
point[size]=x;
s[size]=s[size-1]+tree::size[x]-tree::size[tree::son[x]];
}
return buildBST(1,size,point,s);
}
链上修改
以例题中,将 \(1\sim x\) 链上所有点权增加 \(k\) 为例。
从 \(x\) 开始暴力往上跳,跳到一个节点,那么就是对应树剖中操作重链上所有深度小于等于 \(x\) 的点,即操作全局平衡二叉树上 \(x\) 及其左子树。
那么就可以维护左子树信息,操作时向上合并信息即可。可以写标记永久化。
打一个 \(\textit{tag}_x\) 表示以 \(x\) 为根节点的整个子树增加的懒标记。记 \(\textit{value}_x\) 表示 \(x\) 表示以 \(x\) 为根节点的整个子树的权值和。记 \(\textit{size}'_x\) 为 \(x\) 子树大小。(注意:这里的「子树」不包括轻边及其延伸子树,即只包括「重边」。)
参考代码
void add(int x,int k){
int pl=0;
bool flag=true;
while(x){
//标记永久化
t[x].value=(t[x].value+pl)%P;
if(flag){//不是左子节点(左子节点会被上面的某一个节点计算贡献)
t[x].tag=(t[x].tag+k)%P;
if(t[x].rChild){
t[t[x].rChild].tag=(t[t[x].rChild].tag-k)%P;
}
//标记永久化
pl=(pl + k*(t[t[x].lChild].size+1) )%P;
t[x].value=(t[x].value - k*t[t[x].rChild].size)%P;
}
flag=(x!=t[t[x].father].lChild);
//轻边,标记清空
if(flag && x!=t[t[x].father].rChild){
pl=0;
}
x=t[x].father;
}
}
链上查询
几乎一样,查询左子树和节点信息即可。
参考代码
int query(int x){
int ans=0;
bool flag=true;
int pl=0;//左子树总大小
while(x){
if(flag){
ans=(ans+t[x].value-t[t[x].rChild].value)%P;
ans=(ans-1ll*t[t[x].rChild].size*t[t[x].rChild].tag)%P;
pl=(pl + 1 + t[t[x].lChild].size)%P;
}
ans=(ans+1ll*pl*t[x].tag)%P;
flag=(x!=t[t[x].father].lChild);
if(flag && x!=t[t[x].father].rChild){
pl=0;
}
x=t[x].father;
}
return ans;
}
例题 AC 代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
#include<set>
using namespace std;
constexpr const int N=50000,M=50000,P=201314;
int n,m,ans[M+1],z[N+1];
vector<int>pl[N+1],pr[N+1];
namespace tree{
int father[N+1],size[N+1],son[N+1];
//这里的 g 由于这个题目是有根树,是不包含父节点的,但是如果是无根树就应当包含父节点.
//为了适用性,以下代码均视为包含父节点处理.
vector<int>g[N+1];
void dfs(int x,int fx){
father[x]=fx;
size[x]=1;
for(int i:g[x]){
if(i==fx){
continue;
}
dfs(i,x);
if(size[i]>size[son[x]]){
son[x]=i;
}
size[x]+=size[i];
}
}
int main(){
dfs(1,0);
return 0;
}
}
namespace globalBST{
struct node{
int father,lChild,rChild;
int value,tag,size;
}t[N+1];
int buildBST(int l,int r,int point[],int s[]){
if(r<l){
return 0;
}
int q=lower_bound(s+l,s+r+1,s[r]+s[l-1]>>1)-s;
int p=point[q];
t[p].lChild=buildBST(l,q-1,point,s);
t[t[p].lChild].father=p;
t[p].rChild=buildBST(q+1,r,point,s);
t[t[p].rChild].father=p;
t[p].size=t[t[p].lChild].size+t[t[p].rChild].size+1;
return p;
}
int build(int x){
int y=x;
do{
for(int i:tree::g[y]){
if(i==tree::son[y]||i==tree::father[y]){
continue;
}
t[build(i)].father=y;
}
y=tree::son[y];
}while(y);
static int point[N+1],s[N+1];
int size=0;
for(;x;x=tree::son[x]){
size++;
point[size]=x;
s[size]=s[size-1]+tree::size[x]-tree::size[tree::son[x]];
}
return buildBST(1,size,point,s);
}
int main(){
build(1);
return 0;
}
void add(int x,int k){
int pl=0;
bool flag=true;
while(x){
t[x].value=(t[x].value+pl)%P;
if(flag){
t[x].tag=(t[x].tag+k)%P;
if(t[x].rChild){
t[t[x].rChild].tag=(t[t[x].rChild].tag-k)%P;
}
pl=(pl + k*(t[t[x].lChild].size+1) )%P;
t[x].value=(t[x].value - k*t[t[x].rChild].size)%P;
}
flag=(x!=t[t[x].father].lChild);
if(flag && x!=t[t[x].father].rChild){
pl=0;
}
x=t[x].father;
}
}
int query(int x){
int ans=0;
bool flag=true;
int pl=0;
while(x){
if(flag){
ans=(ans+t[x].value-t[t[x].rChild].value)%P;
ans=(ans-1ll*t[t[x].rChild].size*t[t[x].rChild].tag)%P;
pl=(pl + 1 + t[t[x].lChild].size)%P;
}
ans=(ans+1ll*pl*t[x].tag)%P;
flag=(x!=t[t[x].father].lChild);
if(flag && x!=t[t[x].father].rChild){
pl=0;
}
x=t[x].father;
}
return ans;
}
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=2;i<=n;i++){
int x;
cin>>x;
x++;
tree::g[x].push_back(i);
}
tree::main();
globalBST::main();
for(int i=1;i<=m;i++){
int l,r;
cin>>l>>r>>z[i];
l++,r++,z[i]++;
pl[l-1].push_back(i);
pr[r].push_back(i);
}
for(int i=1;i<=n;i++){
globalBST::add(i,1);
for(int j:pl[i]){
ans[j]=(ans[j]-globalBST::query(z[j]))%P;
}
for(int j:pr[i]){
ans[j]=(ans[j]+globalBST::query(z[j]))%P;
}
}
for(int i=1;i<=m;i++){
if(ans[i]<0){
ans[i]+=P;
}
cout<<ans[i]<<'\n';
}
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
任意链上信息维护
上述都是 \(1\sim x\) 的链,但是有时需要维护 \(u\sim v\) 的链上信息。
当然可以分别维护 \(1\sim u,1\sim v,1\sim\operatorname{lca}(u,v),1\sim\textit{father}'_{\operatorname{lca}(u,v)}\) 的信息,但是这样常数未免过大。
也可以考虑先从 \(u,v\) 更新到 \(\operatorname{lca}(u,v)\),随后继续向上更新信息。常数小一些,但是上面更好些。

浙公网安备 33010602011771号