考虑一下,我们现在想对一棵树进行树链剖分,那么我们首先要知道这棵树的一些信息,比如每一个节点的重儿子是谁,每一个节点的父节点是谁,节点深度,以及每一个节点对应到线段树上的标号也就是这棵树的
d
f
s
dfs
dfs序
要想得到这些信息,我们需要对这棵树进行搜索,以下图为例
现在的标号是树的节点的序号,而不是线段树维护的序列,所以我们需要把这些序号对应到线段树上去,也就是重写这些序号,这个操作在第二次
d
f
s
dfs
dfs中完成
两次搜索
这两次搜索分别处理轻重儿子和线段树标号的问题
dfs1
s
z
[
i
]
sz[i]
sz[i]表示
i
i
i节点的子树节点个数,
d
e
p
t
h
[
i
]
depth[i]
depth[i]表示
i
i
i节点的深度,
f
[
i
]
f[i]
f[i]表示节点
i
i
i的父亲节点,
s
o
n
[
i
]
son[i]
son[i]表示
i
i
i节点的重儿子,进行一次
O
(
n
)
O(n)
O(n)的
d
f
s
dfs
dfs,
int son[MAXN];int depth[MAXN];int f[MAXN];int sz[MAXN];voiddfs1(int u,int fa,int dep){
sz[u]=1;
f[u]= fa;
depth[u]= dep;int maxson =-1;for(int i=head[u];~i;i=edge[i].next){int v = edge[i].to;if(v == fa)continue;dfs1(v, u, dep +1);
sz[u]+= sz[v];if(sz[v]> maxson){
maxson = sz[v];
son[u]= v;}}}
稍微解释一下
m
a
x
s
o
n
maxson
maxson,它的意思是当前节点
u
u
u的可能的重儿子的子树节点数,直到遍历完
u
u
u的所有儿子,最后确定重儿子到底是谁
dfs2
我们得到
d
f
s
1
dfs1
dfs1中的信息之后,开始
d
f
s
2
dfs2
dfs2,这一次我们的目的是找重链,并按照轻重链重新给树上的节点标号,如下图
这里面我标记红色的就是重链
那么进行这样的轻重链剖分的好处是什么呢,或者说有什么用
我们可以发现,现在这棵树已经被划分成了若干条重链,一条重链上的序号都是连续的,同一棵子树里面的节点的
d
f
s
dfs
dfs序也是连续的,我们就可以利用这个性质来进行线段树的维护
int top[MAXN];int wt[MAXN];int id[MAXN];int Data[MAXN];int dfn;voiddfs2(int u,int topf){
top[u]= topf;
id[u]=++dfn;
wt[dfn]= Data[u];if(!son[u])return;dfs2(son[u], topf);for(int i=head[u];~i;i=edge[i].next){int v = edge[i].to;if(v == son[u]|| v == f[u])continue;dfs2(v, v);}}
t
o
p
[
i
]
top[i]
top[i]表示
i
i
i节点所在重链的顶端节点,
i
d
[
i
]
id[i]
id[i]表示
i
i
i号节点经过重新编号之后所对应的号码,
d
f
n
dfn
dfn为时间戳也就是
d
f
s
dfs
dfs序,
D
a
t
a
[
i
]
Data[i]
Data[i]表示开始的点权,
w
t
[
i
]
wt[i]
wt[i]表示经过重新编号之后的点权
程序应该比较好理解,不做解释
线段树维护
首先线段树我们应该是会的,那么新树已经产生了,现在我们想把每一条链都用线段树进行维护,正好维护的是
1
−
n
1-n
1−n这么一个区间,因此,我们可以设立如下的线段树结构,并建立一棵线段树
经过上面的考虑,可以发现一个问题,如何去把不同的链合并,这里就要用到
t
o
p
top
top数组了,因为它记录的是当前重链最顶端的节点,或者说深度最小的节点,利用它就可以找到重链的两个端点,这样就可以实现区间维护了,如果设最下面的节点是
u
u
u,最后这一条链维护完以后,应该把
u
u
u更新为
f
[
t
o
p
[
u
]
]
f[top[u]]
f[top[u]],原因是显而易见的
voidUpdate(int Root,int L,int R,int a,int b,int k){if(a <= L && b >= R){
segtree[Root].value +=(R - L +1)* k;
segtree[Root].lazytag += k;return;}Push_Down(Root, R - L +1);int mid =((R - L)>>1)+ L;if(a <= mid)Update(Root <<1, L, mid, a, b, k);if(b > mid)Update(Root <<1|1, mid +1, R, a, b, k);Push_Up(Root);}voidTree_Update(int u,int v,int add){
add %= MOD;while(top[u]!= top[v]){if(depth[top[u]]< depth[top[v]])swap(u, v);Update(1,1, n, id[top[u]], id[u], add);
u = f[top[u]];}if(depth[u]> depth[v])swap(u, v);Update(1,1, n, id[u], id[v], add);}
T
r
e
e
_
U
p
d
a
t
e
Tree\_Update
Tree_Update写法类似于
L
C
A
LCA
LCA(最近公共祖先),不过为了方便如果处于不同的重链,要始终保持左面的居于深度较大的位置,如果是在同一条重链上,要保持左面的居于深度较小的位置
这里面要特别注意到底是原来的节点编号还是后来的节点编号,这也和自己的写法有关系
接下来是操作2,这和操作1的思路是一样的,不过是求区间和,属于线段树的知识点,这里不提
intQuery(int Root,int L,int R,int a,int b){if(a <= L && b >= R){return segtree[Root].value;}Push_Down(Root, R - L +1);int mid =((R - L)>>1)+ L;int ans =0;if(a <= mid) ans +=Query(Root <<1, L, mid, a, b);
ans %= MOD;if(b > mid) ans +=Query(Root <<1|1, mid +1, R, a, b);
ans %= MOD;return ans;}intTree_Query(int u,int v){int ans =0;while(top[u]!= top[v]){if(depth[top[u]]< depth[top[v]])swap(u, v);
ans +=Query(1,1, n, id[top[u]], id[u]);
ans %= MOD;
u = f[top[u]];}if(depth[u]> depth[v])swap(u, v);
ans +=Query(1,1, n, id[u], id[v]);
ans %= MOD;return ans;}
接下来是操作3,刚才我们提到,在一个子树内,编号是连续的,所以可以使用线段树维护,可以再观察一下刚才那个图,同一棵子树内,根节点的编号加上它的子树节点个数
−
1
-1
−1正好是子树节点编号最大的那一个,所以我们更新的范围应该是
[
i
d
[
i
]
,
i
d
[
i
]
+
s
z
[
i
]
−
1
]
[id[i],id[i]+sz[i]-1]
[id[i],id[i]+sz[i]−1],为什么要减1,因为子树的定义是根节点
u
u
u及其后代的导出子图,包括了根节点