线段树与树链剖分

【前置知识】

运算律,单位元。

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

单位元:

假设我们现在有一个运算 \(\bigoplus\)

如果 \(a\bigoplus b=b\bigoplus a=a\),称 \(b\) 是运算 \(\bigoplus\) 的单位元。

【线段树】

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

  1. 满二叉树;

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

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

  4. 常考,码量大。

【线段树的设计】

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

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

\(n\) 为补充后的元素个数。

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

递归建立线段树:

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

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

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

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

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

【单点查询】

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

递归查询:

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

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

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

【单点修改】

与单点查询差别不大。

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

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

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

注意最后要重新更新。

复杂度:同上,也是 \(O(\log n)\)

【区间查询】

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

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

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

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

复杂度:还是 \(O(\log n)\)

注意到一个性质:

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

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

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

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

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

【区间修改】

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

现在,我们来讲一讲线段树的精华—— 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\) 就到了另一条链上。

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

【先重子深搜序】

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

记录 \(dfn\)\(rev\),分别记录结点在先重子深搜序中的位置,和先重子深搜序中的位置对应的结点。

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

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

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

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

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

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

posted @ 2024-02-15 11:10  FLY_lai  阅读(8)  评论(0编辑  收藏  举报