全局平衡二叉树

本文中的「重子节点」、「轻边」等术语定义同重链剖分。


全局平衡二叉树,即在重链剖分的基础之上,使用静态二叉树来维护重链,从而得到的常数优秀、复杂度 \(\mathcal O(\log n)\) 的数据结构。

全局平衡二叉树可以用于在 \(\mathcal O(\log n)\) 的时间内处理树上修改/查询,可以代替树剖维护链上信息。(其实也可以维护子树信息,但是我不会。)

全局平衡二叉树

例题:[LNOI2014] LCA

给定一棵以 \(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)\) 实现这些操作。

主要性质

先看一个例子。

使用实线表示「重边」,虚线表示「轻边」。

例如,有这样一棵树:

010

其全局平衡二叉树为:

011

  • 全局平衡二叉树是由若干棵静态二叉树组成的树,这些二叉树由内向轻边相连。

  • 全局平衡二叉树的每一棵二叉树都维护一条重链的信息。其中序遍历是原重链按照深度单调递增得到的序列。

  • 全局平衡二叉树的边分轻边和重边。重边是正常的边,也是原重链中的重边;轻边均为单向边,均从一棵二叉树的根节点指向其父节点(即「内向」)。(注意,「重子节点」和「轻子节点」一般情况下还是取重链剖分意义下的对应节点。)

  • 全局平衡二叉树的树高为 \(\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\)。在代码中,这一部分存储在名为 treenamespace 中。

从根节点 \(1\) 开始,对于当前节点 \(x\) 所在的重链上所有节点的轻子节点递归建树。

递归结束后,便是对当前节点 \(x\) 所在重链部分(不一定完整)建树。

\(y_1,y_2,\cdots,y_k\) 是当前重链上按照深度从小到大排序后的节点。

对于重链上的节点 \(y_i\),可以计算 \(y_i\) 的权值:

\[s_i=s_{i-1}+\textit{size}_{y_i}-\textit{size}_{\textit{son}_{y_i}} \]

特别地,\(s_0=0\)

不妨设当前子树由 \(y_l\sim y_r\) 构成。

那么,若 \(i\) 满足:

\[s_i\leq\dfrac{s_r+s_{l-1}}2\leq s_{i+1} \]

则,\(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)\),随后继续向上更新信息。常数小一些,但是上面更好些。

posted @ 2025-07-31 15:36  TH911  阅读(22)  评论(0)    收藏  举报