线段树与树链剖分

【前置知识】

运算律,单位元。

运算律基本就是交换律、结合律、分配律。

单位元:

假设我们现在有一个运算

如果 ab=ba=a,称 b 是运算 的单位元。

【线段树】

线段树是一个树形数据结构。

  1. 满二叉树;

  2. 倍增的思想,分治的手法;

  3. 点/区间进行查询/修改;

  4. 常考,码量大。

【线段树的设计】

为了满足线段树满二叉树的定义,我们先把数组补成 2的幂 个。

补充的数肯定不能影响我们的计算,于是我们把它们设成单位元。

n 为补充后的元素个数。

我们让这 n 个元素组成线段树的叶结点们。

递归建立线段树:

  1. 如果当前节点就是叶结点,用给好的元素组成;

  2. 否则递归建立左右子结点,然后用子结点的属性更新自身。

根结点代表区间 [1,n+1)

凡是有子结点的结点,设其代表区间 [l,r)m=(l+r)2,则其左儿子代表区间 [l,m),右儿子代表区间 [m,r)

注意:线段树有 2n1 个结点,而一开始可能补了一倍,再翻倍,说明线段树最多会开出四倍的空间。

【单点查询】

不妨我们现在查询 p 号元素的属性。

递归查询:

  1. 如果当前节点是叶子结点,返回当前节点的属性;

  2. 否则设当前节点代表区间 [l,r)m=(l+r)2,如果 p<m,那么递归进入左子结点查询;否则进入右子结点。

复杂度:显然每次区间长度除以 2,O(logn)

【单点修改】

与单点查询差别不大。

  1. 如果当前节点是叶子结点,返回当前节点的属性;

  2. 否则设当前节点代表区间 [l,r)m=(l+r)2,如果 p<m,那么递归进入左子结点查询;否则进入右子结点。

  3. 递归结束之后,重新更新当前节点的属性。

注意最后要重新更新。

复杂度:同上,也是 O(logn)

【区间查询】

设我们要查询区间 [l,r)

  1. 如果当前结点代表的区间完全被 [l,r) 包含,直接返回当前节点的属性;

  2. 如果当前节点代表的区间与 [l,r) 完全无交,直接返回单位元;

  3. 否则递归查询左右儿子的答案,然后合并左右儿子的答案并返回。

复杂度:还是 O(logn)

注意到一个性质:

一个区间,只有包含左右端点的结点可能递归。

那么每层递归的区间个数最多两个。

也就是说我们最多需要处理四个。

递归到叶结点,也只是 O(logn),省略 4 的常数,整体复杂度还是 O(logn)

到这里,看做题总结的【无区间修改】部分。

【区间修改】

树状数组也可以做到区间查询、单点修改,但是以上都没有做到。

现在,我们来讲一讲线段树的精华—— lazy tag

【lazy tag】

先说一说想法。

区间查询的瓶颈在于,我们需要深入到每个叶结点进行修改。

于是,我们决定不要修改所有叶结点,而是把修改的信息记录下来,等到需要的时候再拿出来用。

于是,lazy tag 就诞生了。

lazy tag,又称懒标记,延迟标记。

每个结点上都有一个 lazy tag,记录着节点代表区间的修改信息。

比如,我现在有一个 8个元素的数组。

现在,我想把 [1,5) 加 4,我就只把 [1,5) 对应结点的 lazy tag 加上 4,并且把该节点的和也加上 4 × 4。

很显然,如果我要修改 [2,6) 的话,就没有现成的结点了。

因此我们有了一个修改的思路:

  1. 如果当前区间完全被修改区间包含,直接在本结点上更新标记;

  2. 如果当前区间与修改区间完全无交,直接返回;

  3. 否则递归给左右儿子修改,并且改完之后用左右儿子的属性更新本身。

而查询,我们也顺水推舟:

单点:

  1. 如果当前结点是叶结点,返回当前结点的值加 tag;

  2. 哪边有要查询的去哪;

  3. 返回的时候返回 子结点返回的结果+本结点的tag。

区间:

  1. 如果当前区间被完全包含,直接返回当前节点的属性;

  2. 如果完全无交,返回单位元;

  3. 递归查询左右儿子,并应用本结点的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,表示当前节点所在链的链顶。

显然这是一个自上而下传递的属性。

因此我们需要两边搜索,第一次找重子,第二次找 top。

【树链剖分找LCA】

显然当两个结点在同一条链上时,它们的 LCA 是较高的那个点。

所以:当两个结点的 top 相等,返回较高的那个。

否则:因为只有两个结点在同一条链上时,才有可能一个是另一个的 LCA,于是我们可以让链顶较深的结点 x 跳到 p[top[x]],这样 x 就到了另一条链上。

循环往复,一定会到同一条链上。

【先重子深搜序】

我们深搜时优先走重子,得到了先重子深搜序。

记录 dfnrev,分别记录结点在先重子深搜序中的位置,和先重子深搜序中的位置对应的结点。

我们发现,一棵子树和一条树链,在深搜序中是连续的。

因此我们可以用线段树维护。

比如我们要把一条路径 (u,v) 上的结点权值都+1。

按照上面找LCA的方法,只是在跳到链顶的时候利用线段树和 rev 数组把 当前节点 到 当前节点链顶 的这一段 进行区间修改。

注意:同链比自身深度,异链比链顶深度。左闭右开

复杂度分析:子树是一个区间,复杂度贡献 O(log);如果要处理链,最多有 O(logn) 个区间,因为每作为一次轻儿子,节点个数至少两倍。所以链贡献复杂度 O(log2n)

posted @   FLY_lai  阅读(22)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示