树链剖分
本文章遵守知识共享协议 CC-BY-NC-SA ,转载时须在文章的任一位置附上原文链接和作者署名(rickyxrc)。推荐在我的个人博客阅读。
大意
树链剖分,是一种将一棵树转化为线性的一种算法,可以方便地计算一颗子树的区间和。
前置知识
- 线段树
- 多叉树
树链剖分需要两次dfs操作,第一次用于更新深度广度,第二次用于真正的剖分。
第一次搜索代码
void first_dfs(int index,int fath){
deps[index] = deps[fath]+1; // 该点深度等于父亲深度+1
fa[index] = fath; // 设置该点父亲
size[index] = 1; // 设置当前儿子数量
for(auto s:edge[index]){ // 遍历每一条边
if(s == fath)continue; // 不能搜回去
first_dfs(s,index); // 继续向下搜索
size[index] += size[s]; // 更新儿子数量
if(size[hson[index]] < size[s])
hson[index] = s; // 设置重儿子,以方便重链更新。
}
}
第二次搜索代码
void treecut(int index,int topp){
top[index] = topp; // 当前链上最高的点
id[index] = ++liancnt; // 当前点新建一条链
rev[liancnt] = index; // 反向索引更新
if(hson[index] == 0)return; // 如果为叶子节点则返回
treecut(hson[index],topp); // 沿重儿子向下dfs
for(auto s:edge[index]) // 遍历每一条边
if(s!=hson[index] && s!=fa[index]) // 不是重儿子 且 没有往回搜索
treecut(s,s); // 搜索轻儿子
}
此时,树链剖分的结果就会保存在 rev 数组中(将一棵树变为线性的结果)。
树链剖分的另一个作用便是求最近公共祖先(LCA)。
inline int LCA(int a,int b){
while(top[a] != top[b]){ // a与b不在一条重链上
if(deps[top[a]] > deps[top[b]]) // 链顶深度较深的点向上
a = fa[top[a]]; // 注意是 fa[top[a]]
else
b = fa[top[b]];
}
return deps[a]<deps[b] ? a:b; // 深度较浅的就是LCA
}
查询一条链的和
我们需要先写几个辅助函数
只有一条链上的值可以用线段树直接加!
单链加(一条重链上的两个点及之间加)
inline void add(long long x,long long y,long long val){
if(deps[x]>deps[y]) // 判断高低情况
Seg::updateSegment(id[y],id[x],val); // 直接加
else
Seg::updateSegment(id[x],id[y],val);
}
单链查(一条重链上的两个点及之间查询)
inline long long query(long long x,long long y){
if(deps[x]>deps[y]) // 判断高低情况
return Seg::querySegment(id[y],id[x]); // 直接查
else
return Seg::querySegment(id[x],id[y]);
}
简单来说就是在LCA时加链上的内容,所以和LCA的代码没有很大的区别。
代码如下:
inline void chainadd(long long a,long long b,long long val){
while(top[a] != top[b]){
if(deps[top[a]] > deps[top[b]])
add(top[a],a,val), // 这里
a = fa[top[a]];
else
add(top[b],b,val), // 这里
b = fa[top[b]];
}
add(a,b,val); // 这里千万不要忘!
return;
}
更新链
思路相同,不再赘述。
inline long long chainquery(long long a,long long b){
long long ans=0;
while(top[a] != top[b]){
if(deps[top[a]] > deps[top[b]]){
ans += query(top[a],a);
a = fa[top[a]];
}
else{
ans += query(top[b],b);
b = fa[top[b]];
}
}
ans += query(a,b);
return ans;
}
查询子树和
根据上文所提及的特性(树中的任意一颗子树在剖分完成的区间中连续),则可以写出。
可能唯一的问题是定位区间,但是在第一次 dfs 时就已经初始化了每颗子树的 size ,所以可以 O(1) 定位区间。
inline long long queryTree(long long x){
return Seg::querySegment(id[x],id[x] + size[x] - 1);
}
inline long long addTree(long long x,long long y){
Seg::updateSegment(id[x],id[x] + size[x] - 1,y);
}
模板题目及解析
P3379-【模板】最近公共祖先-LCA
这道题一看就是裸的树剖,直接打板子就行。
树链剖分完成之后,剖分完成的树具有一些性质,利用这些性质我们可以进行树上解题。
P3384-【模板】轻重链剖分/树链剖分
剖分完成的树序列的其中一个性质:树中的任意一颗子树在剖分完成的区间中连续。
另一个显而易见的性质: 一条链在剖分完成的区间中连续。
所以,树链剖分结合线段树,就可以进行树上求和等操作。
线段树的值需要先进行映射,这样才能将树链剖分的优势发挥出来。
for(long long i=1;i<=n;i++)
t_indexed[i] = t[rev[i]]; // 通过rev数组映射
initSegmentTree(1,n,t_indexed);

浙公网安备 33010602011771号