树链剖分
本文章遵守知识共享协议 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);
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用