线段树

概述

  • 线段树通过在原数组上建一棵二叉树,将原序列不断二分治,以高效地处理各种结合性问题。

  • 但显然仅分治虽然可以通过把询问区间拆成多个线段树区间的并以实现快速查询,却不能快速修改,还是得一路分治到底。

  • 故线段树引入了作用于原信息的懒标记,将修改操作延迟到需要访问对应信息的时候,某种意义上是把修改的复杂度均摊到询问上。

  • 显然这需要懒标记满足可结合性,即旧标记和新标记必须不仅能共存,而且能合并,否则下推标记的复杂度无法接受。

  • 实质上,这代表着修改操作需要满足结合律,因为不同的下推顺序可能导致修改间的结合顺序不同。

  • 事实上,线段树所能维护的信息和修改必须满足(信息,标记对信息的作用)构成一个半群,(标记,修改)构成一个幺半群。这里幺与否我不太确定。实践中往往会强一些,会是群。如果强到交换群的程度,就可以考虑使用树状数组代替线段树。

  • 由其二分治结构,各种简单操作都可以在 \(O(\log)\) 的时间内实现,部分特殊操作需要 \(\Omega(\log)\sim O(\log^2)\) 的时间。空间复杂度视实现而定,静态开点(满二叉树式存储)为 \(O(n)\),动态开点(取 \(v\) 为点的值域)\(O(n\log_2 v)\)。实际使用中一般开 \(4n\) 的大小。

实现原理

  • 我们定义一个“管辖范围”为线段树节点(下简称节点)对应的原数组的区间。每个节点都应当存储着它的子树的总信息,即,按题目需求结合起来的信息。

  • 显然它的根的管辖范围为 \(1\sim n\) 。作为一棵二叉树,线段树上的非叶节点都将自己的管辖区间一分为二分给左右儿子,一般是 \(l\sim mid\)\(mid+1\sim r\),其中 \(mid\) 为分治界,通常取 \(mid=\lfloor\frac{l+r}{2}\rfloor\)

  • 静/动态开点线段树有着不一样的存点方式。静态开点线段树使用类同满二叉树的存储方式,动态开点线段树每次插入或建树时访问到未建出的节点就将它新建出来并连边。

  • 下面给出懒标记的定义:如果还不需要访问某一个节点的子树,则将它子树应该进行的修改存在它的 \(lazy\) 上,直到需要用到时再下传标记进行修改。

  • 如果是整体用到这棵子树,可以利用它本身的信息加上可以直接算出的修改影响来作答。当然,某些操作,如下取整除法,开方等的影响无法算出,所以必须数据合适(例如这两者都可以证明单点的修改次数不超过 \(O(\log)\),故可以使用线段树,此时的修改本质是暴力)。

  • 注意,下传标记进行修改并不是一修到底!事实上,我们每次都只修改下一层的节点,然后把 \(lazy\) 标记放在下一层的节点上。

  • 线段树的操作大体上可以分为三种:修改,查询,维护。

    • 维护:以仅询问区间求和为例。在修改后,将当前节点的 \(sum\) 维护成 \(sum_{ls}+sum_{rs}\),保证每个节点有正确的其管辖范围的结合性信息。

    • 修改:以区间修改为例。我们需要传 \(6\) 个参数,分别为 \(now,l,r,L,R,val\),代表当前在哪个节点,当前节点的管辖范围的左右端点,当前修改的区间的左右端点,对应区间将受的修改的值。

    • 从根节点开始修改,此时参数应该为 \((rt,1,n,L,R,val)\)。每到一个节点,首先判断它的管辖范围是否完全在修改范围内,如果是,则打标记,返回。如果不是,则判断左右子树管辖范围是否与之重合,如果是,则递归求解,然后维护当前节点信息。

    • 询问:以询问区间求和为例。模仿修改,如果被包含直接回答,否则递归左右子树,回答两者之和。

复杂度证明

  • 维护不计(归到修改和询问上作常数),单点修改/询问显然是 \(\log_2 m\)(跑满一条链)级别的,重点证明一下区间操作的复杂度,以区间询问为例。

  • 我们把所有 return 的点定义为“到达点”,经过而没有 return 的点定义为“经过点”。

  • 定理 1:任意 \(2\) 个“到达点”不为兄弟,即两者不拥有相同的直接父亲。

    • 假设有至少 \(2\) 个到达点是兄弟,那么根据询问逻辑,应当在父亲处被合并作答,不成立。

    • 故定理 1 成立。

  • 定理 2:将所有到达点的管辖范围长度提取出来,得到的必然是一个广义单峰数列。

    • 假设该数列除了单峰处外,还有其他转折然后递降的点,则必然形成谷。

    • 对于一个谷点,我们找到它在原线段树上的对应节点,不断回溯直到它有至少一个兄弟(如果不回溯就有兄弟则违反定理 1)。

    • 则此时它子树内有且仅有谷点代表的一段被询问,从而必然有至少一段没有被询问。

    • 但我们知道谷点是在数列中间的。换句话说,整个询问区间可以做一定的拆分,使得在中间空出了一块。

    • 这与区间询问的区间必须连续相矛盾,所以不成立。

    • 所以定理 2 成立。

  • 定理 3:任意一层不会有超过 \(2\) 个“到达点”。

    • 假设有至少 \(3\) 个节点。由定理 1,三者互相不为兄弟。

    • 由定理 2,三者又必须在广义单峰数列中连续,否则必然导致谷的出现。则三者连续,至少有两者为兄弟。

    • 矛盾,所以定理 3 成立。

  • 推论 1:因为线段树至多 \(\log_2 m\) 层,到达点总数不超过 \(2\log_2 m\)

  • 推论 2:提取出最左和最右的到达点,将根到他们的路径称为“主路径”,则其他到达点能且仅能经过 \(1\) 条边到达主路径。

    • 能经过 \(1\) 条边:

      • 假设某个到达点至少要经过 \(2\) 条边才能到达主路径。

      • 则对其进行回溯,必然能找到一个节点不属于主路径同时又是该节点的祖先。

      • 这个祖先的管辖范围一定有至少一段没有被包含,从而没有被询问。

      • 但我们又知道这不是第一个/最后一个到达点(也即询问区间),则这与区间询问的区间必须连续矛盾,不成立。

    • 只能经过一条边:这是个二叉树,树上路径唯一。

  • 推论 3:单次询问访问到的点不超过 \(4\log_2 m\)

    • 两条主路径 \(2\log_2 m\),中间的到达点数小于 \(2\log_2 m\)
  • 综上所述,我们证明了区间操作的复杂度也是严谨单次 \(O(\log m)\) 级别的;另一方面我们也认识到,线段树的常数确实比树状数组大上不少。

  • 这里是应该有图的,但手搓图非常麻烦(这需要一个特别大的图),而且这个没有树状数组那么不显然,所以无限期推迟。

更多实现

  • 线段树的标记永久化:一种对询问的优化。

    • 更改询问方式。询问子树内单点时,每回溯一步,就将对应标记的影响赋到 \(ret\) 上。

    • 于是永远都不需要下推标记,时间复杂度得到了优化。

    • 主要运用于主席树。应当指出的是,这可能导致对信息更严格的要求,譬如标记对信息的影响必须满足交换律(这条似乎不是可能)。

  • 多序列:考虑对序列 \(a,b\) 做区间修改和区间点积和查询。

    • 维护 \(sum_a,sum_b,sum_{ab}\),考虑对 \(a+v\),于是 \(sum_{ab}=sum_{ab}+sum_b\times v\)

    • 可以推广到任意个序列,但显然需要维护 \(2^m+1\) 种信息,单次修改要改动的信息也很多,不想算了,总之不太实用。

    • 应该也可以以同样的思路推广到其他简单操作。目前还没怎么写过,之后再说吧。

    • 对于非经典操作,目前只知道 \(\min(a,b)\) 这类好像是李超树。

  • 猫树(静态高速询问线段树):一种在不修改前提下对询问的优化。

    • 考虑在每个节点维护前后缀答案。显然这是 \(O(n\log)\) 的。

    • 询问时,快速找到 \(l,r\) 在线段树上的 lca,只做一次结合。

    • 显然快速求 lca 是个问题。一个较为经典的解决方式是堆式建树,即将线段树的值域上取整到二的整数次幂,于是任意两点的 lca 为其二进制串的 lcp,这可以通过右移异或结果以二为底的对数来实现,于是可以 \(O(1)\) 求 lca。

    • 这一实现还可以更毒瘤(也更魔怔)。注意到分块处理前后缀然后对任意一段(若干个连续块)维护整体信息也可以 \(O(1)\) 回答块间询问,于是考虑将两者结合,可以一路冲到 sqrt tree,但显然魔怔过头了,至少对现在的我来说。搁置。

例题

区间判重

  • 代表题目:\(\text{P3792 由乃与大母神原型和偶像崇拜}\)\(\text{P5278 算术天才9与等差数列}\),及稍有推广的 \(\text{P6617 查找 Search}\)

  • 题意:求区间 \([l,r]\) 内是否存在一对 \(match\),这里的 \(match\) 可以是相同数,也可以是别的什么乱七八糟的定义。带单点修改。

  • 考虑使用线段树维护数组 \(with\)\(with_i\) 表示 \(<i\) 且能与 \(i\) 匹配的最大位置,于是问题变成 \([\max_{i=l}^r with_i\geqslant l]\)

  • 注意到这样修改量可能过大,譬如考虑令后 \(\dfrac{n}{2}\) 完全相同,开始时它们都没有匹配,然后依次将 \(1\sim \dfrac{n}{2}\) 修改成和它们匹配的,每次都有 \(O(n)\) 的修改量。这里的连续性显然是特例,不起作用。

  • 故改定义,若有多个 \(with_i=j\),则令最小的 \(i\)\(with=j\),其他的失配。可以证明,这不影响上面的问题转化,因为当且仅当最小的 \(i\) 被包含,对应 \(j\) 才可能被包含。

  • 考虑怎么进行单点修改。在匹配对象可以 \(O(1)\) 计算出来的时候,我们通常使用 set 维护每个数字的出现位置,然后修改。这里以 \(match\) 的对象为相同数字为例,将修改拆成删除和插入:

    • 删除:令后继与前驱匹配(注意这种匹配是单向的)。

    • 插入:令后继与当前匹配,当前与前驱匹配。

  • 推广到更为一般的情况,如查找那道题,就可能要考虑当前、前驱、后继、匹配的前驱、匹配的后继,较为复杂,主要有两种较为方便的实现方式:

    • 格式化分类讨论,即将所有情况都用 if else 列出来,不进行无用情况清除和类同情况合并。优点是清晰,缺点是码量较大。

    • 暴力重构对应区间。优点是好写,缺点是可能常数很大甚至复杂度更高。

  • 可以和 set 结合维护更恶心的东西,事实上这时候线段树更多地只是个背景板,或者说区间求值已经不再是难度所在,例如 \(\text{P5069 [Ynoi2015] 纵使日薄西山}\)

扩展:吉老师线段树

扩展:李超线段树

扩展:主席树

扩展:线段树分裂合并

扩展:线段树相关的树套树

扩展:线段树优化 DP

posted @ 2023-01-07 09:06  未欣  阅读(46)  评论(0编辑  收藏  举报