全局平衡二叉树维护链上信息

例题:洛谷 P4211 [LNOI2014] LCA

Posted by TH911 on July 29, 2025

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


全局平衡二叉树,即在重链剖分的基础之上,使用静态二叉树来维护重链,从而得到的常数优秀、复杂度 $\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)$ 实现这些操作。

主要性质

先看一个例子。

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

例如,有这样一棵树:

其全局平衡二叉树为:

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

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

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

  • 全局平衡二叉树的树高为 $\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)$。

参考代码 ```cpp 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$ 子树大小。(注意:这里的「子树」不包括轻边及其延伸子树,即只包括「重边」。)

参考代码 ```cpp 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; } } ```

链上查询

几乎一样,查询左子树和节点信息即可。

参考代码 ```cpp 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 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
//#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)$,随后继续向上更新信息。常数小一些,但是上面更好些。