线段树与树链剖分
【前置知识】
运算律,单位元。
运算律基本就是交换律、结合律、分配律。
单位元:
假设我们现在有一个运算
如果
【线段树】
线段树是一个树形数据结构。
-
满二叉树;
-
倍增的思想,分治的手法;
-
点/区间进行查询/修改;
-
常考,码量大。
【线段树的设计】
为了满足线段树满二叉树的定义,我们先把数组补成 2的幂 个。
补充的数肯定不能影响我们的计算,于是我们把它们设成单位元。
记
我们让这
递归建立线段树:
-
如果当前节点就是叶结点,用给好的元素组成;
-
否则递归建立左右子结点,然后用子结点的属性更新自身。
根结点代表区间
凡是有子结点的结点,设其代表区间
注意:线段树有
【单点查询】
不妨我们现在查询
递归查询:
-
如果当前节点是叶子结点,返回当前节点的属性;
-
否则设当前节点代表区间
, ,如果 ,那么递归进入左子结点查询;否则进入右子结点。
复杂度:显然每次区间长度除以 2,
【单点修改】
与单点查询差别不大。
-
如果当前节点是叶子结点,返回当前节点的属性;
-
否则设当前节点代表区间
, ,如果 ,那么递归进入左子结点查询;否则进入右子结点。 -
递归结束之后,重新更新当前节点的属性。
注意最后要重新更新。
复杂度:同上,也是
【区间查询】
设我们要查询区间
-
如果当前结点代表的区间完全被
包含,直接返回当前节点的属性; -
如果当前节点代表的区间与
完全无交,直接返回单位元; -
否则递归查询左右儿子的答案,然后合并左右儿子的答案并返回。
复杂度:还是
注意到一个性质:
一个区间,只有包含左右端点的结点可能递归。
那么每层递归的区间个数最多两个。
也就是说我们最多需要处理四个。
递归到叶结点,也只是
到这里,看做题总结的【无区间修改】部分。
【区间修改】
树状数组也可以做到区间查询、单点修改,但是以上都没有做到。
现在,我们来讲一讲线段树的精华—— lazy tag。
【lazy tag】
先说一说想法。
区间查询的瓶颈在于,我们需要深入到每个叶结点进行修改。
于是,我们决定不要修改所有叶结点,而是把修改的信息记录下来,等到需要的时候再拿出来用。
于是,lazy tag 就诞生了。
lazy tag,又称懒标记,延迟标记。
每个结点上都有一个 lazy tag,记录着节点代表区间的修改信息。
比如,我现在有一个 8个元素的数组。
现在,我想把
很显然,如果我要修改
因此我们有了一个修改的思路:
-
如果当前区间完全被修改区间包含,直接在本结点上更新标记;
-
如果当前区间与修改区间完全无交,直接返回;
-
否则递归给左右儿子修改,并且改完之后用左右儿子的属性更新本身。
而查询,我们也顺水推舟:
单点:
-
如果当前结点是叶结点,返回当前结点的值加 tag;
-
哪边有要查询的去哪;
-
返回的时候返回 子结点返回的结果+本结点的tag。
区间:
-
如果当前区间被完全包含,直接返回当前节点的属性;
-
如果完全无交,返回单位元;
-
递归查询左右儿子,并应用本结点的tag。
【pushdown】
但是,我们发现,如果按上面的方法做,可能会出一些问题。
比如赋值运算,新来的修改可能比老修改更靠下,这样在一路应用回去的时候就会被覆盖。
本质在于赋值运算不满足交换律。
于是我们想了一个办法:只要新来的修改到达一个结点,立刻把这个结点的标记分发给左右儿子,直到新修改完毕。
这样操作我们就可以把交换律的问题解决。
去看【有区间修改】
【总结】
三个律:结合律,交换律,分配律。
三个运算:val+val,val×tag,tag×tag。
val + val:结合律。(小区间拼成大区间)
tag × tag:结合律。(val*tag1*tag2=val*(tag1*tag2))
pushdown:为了解决修改不符合交换律的问题。
区间修改:需要满足修改运算对查询运算的分配律,不然小区间拼成大区间,直接 ×tag 没有分配律会出错。
应对措施:
如果没有 val+val 的结合律,区间查询就变成单点查询,再求和。
如果没有 tag×tag 的结合律,在每个结点上记录一个 tag 数组。
如果没有交换律,pushdown。
如果没有分配律,区间修改就变成单点修改。
【树链剖分】
分块思想:把整体分成一个个小块,这样我们每次处理的时候就可以整体处理。
我们考虑把整个树剖分成若干条链。
定义:
一个结点的子结点中,子树规模最大的子结点,称为该节点的“重子”,其余的称为“轻子”,叶结点的重子是本身。
显然一个结点恰有一个重子。
记录重子,我们可以在每个结点的 vector 上通过 swap 把重子换到 0 号位置。
从一个结点通向其重子的边称为重边。
若干条连续重边连起来,就成了链。
树就被剖分成若干条链。
在每个节点上,记录
显然这是一个自上而下传递的属性。
因此我们需要两边搜索,第一次找重子,第二次找 top。
【树链剖分找LCA】
显然当两个结点在同一条链上时,它们的 LCA 是较高的那个点。
所以:当两个结点的 top 相等,返回较高的那个。
否则:因为只有两个结点在同一条链上时,才有可能一个是另一个的 LCA,于是我们可以让链顶较深的结点
循环往复,一定会到同一条链上。
【先重子深搜序】
我们深搜时优先走重子,得到了先重子深搜序。
记录
我们发现,一棵子树和一条树链,在深搜序中是连续的。
因此我们可以用线段树维护。
比如我们要把一条路径
按照上面找LCA的方法,只是在跳到链顶的时候利用线段树和
注意:同链比自身深度,异链比链顶深度。左闭右开
复杂度分析:子树是一个区间,复杂度贡献
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!