线段树

普通线段树

线段树是一个二叉树,一般用于维护数组。我们让每个节点维护其子树所包含的区间的信息,每个节点将其区间分为 \([l, mid]\)\((mid, r]\) 分割给左右儿子维护。

每时每刻,只要左右儿子信息正确无误,则父亲节点的信息可由左右儿子 pushup 合并更正。也就是说区间答案需要满足可合并性。

单点修改区间查询

典中典的线段树维护的是单点修改,区间查询。修改时一路递归到叶子,修改叶子后 pushup 回根节点,更改所有包含该点的区间。

单点修改复杂度 \(O(\log n)\),因为包含一个点的区间不超过 \(O(\log n)\)。(应该是 \(O(w\log n + t\log n)\) 其中 \(O(w)\) 为合并复杂度,\(O(t)\) 为修改复杂度。

区间查询时,将区间拆分成尽量少个线段树上的区间,将多个区间的答案合并。

区间查询复杂为 \(O(\log n)\),因为一个区间分割出来的区间不超过 \(O(\log n)\)。(应该是 \(O(w\log n)\) 其中 \(O(w)\) 为合并复杂度。

区间修改区间查询

最基本的线段树无法支持区修,区修单查也需要用到前缀和来转化成单修区查,这是因为区间修改无法修改所有覆盖的区间。懒标记就是解决这个问题的。

区间修改时,我们与查询相同,将区间拆分成树上 \(O(\log n)\) 个区间,这 \(O(\log n)\) 个区间直接修改维护值,并打上懒标记。对于一个打了懒标记的区间节点,代表节点子树内所有区间都未被正确修改,但懒标记储存了应该的修改量。

同时我们要求访问的区间必须已经是真实值,我们需要额外加入一个 pushdown 函数,用于将懒标记下传到可能访问的区间。具体的对于访问的一个节点,我们首先把懒标记下传到左右儿子,再进行查询/修改操作。

抽象化

我们来探讨线段树维护的信息需要满足什么条件。

  • 【可合并性】可以通过左右儿子的信息求得父亲,通过多个区间的信息求得区间查询的答案。例如区间求和,区间求 max,而区间 mex 则不行。更复杂的区间最大子段和难以直接维护,但可以通过额外维护前缀max和后缀max得到。
  • 【结合律】注意到线段树区间信息的合并顺序并非从左到右,而是二叉树的形式。这导致其无法维护不具有结合律的信息。

如果要支持区间修改,意味着我们需要加入懒标记,懒标记一般与操作相关。懒标记需要满足以下条件:

  • 【可合并性】旧的懒标记需要在修改时能与新的懒标记合并,否则多个懒标记不能优化时间复杂度。
  • 【懒标记与信息的可合并性】当前节点在打标记后需要能快速计算维护的值。例如区间取min区间求和的线段树无法使用简单的懒标记,因为区间取min之后我们不知道具体的和。

用群论的理论来理解。就是我们维护的信息需要满足一个双半群结构。

半群是一个非空集合 \(S\) 以及一个运算符 \(·\),满足:

  • 对于任意 \(S\) 中的两个元素 \(a,b\)\(a·b\) 也在 \(S\) 内。这就是我们说的可合并性。
  • 对于任意 \(S\) 中三个元素 \(a, b, c\)\(a·(b·c)=(a·b)·c\)。也就是结合律。
    额外的若存在单位元 \(e\in S\) 满足对于任意 \(a\in S\)\(a · e = a\),则称其为一个幺半群。(注意 \(a·e\) 一定等于 \(e·a\),具体证明不再赘述)

双半群指的是,区间信息需要为一个半群,标记信息需要为一个幺半群(因为标记信息可以为空)。同时还需要额外满足,区间信息和标记信息运算后得到区间信息。

可以看一下这篇博客:https://www.cnblogs.com/Meatherm/p/17925813.html

值域线段树

某些同学似乎不是非常理解值域线段树究竟是个什么东西,其实我也无法理解为什么他们无法理解(

其实非常简单,值域线段树就是维护数组值域的线段树。

对于一个数组 \(a_n\)\([1, n]\) 为其下标,而 \(a_i\) 的取值范围就是其值域。一个排列的值域为 \([1, n]\)

值域线段树一般可以用于统计数组在值域区间 \([l, r]\) 之内有多少值。通常在扫描线时使用,可以动态维护区间 \([1, i]\) 内每个值出现次数。

动态开点

一般线段树使用左儿子下标为 \(2o\) 子下标为 \(2o + 1\) 的方法保存数据。原因是这样方便并且减少常数。

但是当我们线段树区间范围过大,但是实际使用的节点很少时,我们可以使用动态开点动态分配空间。例如维护值域为 \([1, 1e9]\) ,但只有 \(n \leq 1e5\) 个值的值域线段树。

动态开点只有在区间信息更改至非初始值时才进行节点的创建,这也意味着我们不能使用原来的 \(2o, 2o+1\) 编号方法。

具体来说每个节点需要额外储存左右儿子的编号,直接用指针也行。

可持久化

如果需求查询历史版本则要用到可持久化线段树。

可持久化线段树本质上是压缩信息,每次修改时信息不变的节点与原树共用。

无论是单点修改还是区间修改,我们实际更新的信息都只有 \(O(\log n)\)。对于被修改的节点,我们创建一个新节点储存修改后的值,新节点的左右儿子如果没有被改变,则直接指向原节点,否则指向新节点。

那么每次修改我们都只会新建一条到根的路径,空间和时间复杂度都为 \(O((n + q) \log n)\)

可持久化线段树允许我们访问历史版本,于是我们可以同时访问两个版本,通过两个版本的信息差计算答案。

一个比较经典的题就是区间 k 小值。

给定长为 \(n\) 的数组 \(a\),你需要回答 \(q\) 次询问:

  • 给定 \(l, r\),询问 \(a\) 数组区间 \([l, r]\) 内第 \(k\) 大值。

我们可以维护一个值域上的可持久化线段树,对所有 \(i\in [1, n]\) 分别维护数组中 \([1, i]\) 内每个数出现次数。

每次往 \([1, i-1]\) 的线段树中加入 \(a_i\),得到 \([1,i]\) 的线段树。容易发现每次修改只会修改 \(O(\log n)\) 个信息,所以复杂度为 \(O(n\log n)\)

每次查询 \([l, r]\) 时将 \(r\)\(l - 1\) 两棵线段树上的信息相减就是值 \(x\) 在区间 \([l, r]\) 内出现次数。

一颗子树的出现次数和就是子树代表的值域区间内值在 \([l, r]\) 出现次数。

在遍历线段树的同时二分,就可以求出 \(k\) 小值。

标记永久化

标记永久化是打懒标记的另一种方式。我们不进行标记的下传,而是在查询时再对答案进行修正。例如区间加区间求和,我们只需要在求和返回过程中,把沿途区间的懒标记加入答案。

一般在可持久化线段树上会使用,由于可持久化线段树一个节点可能属于多个时间状态的线段树下,所以进行标记下传会破坏之前时间节点的信息,需要使用标记永久化。

势能线段树

利用题目中的性质,来保证时间复杂度的一种方法。

例如区间开根,区间求和。每次开根时可以暴力递归更新。因为没有其他操作,一个值在 \(O(\log n)\) 次开根操作后会变成 \(1\)。递归时判断如果一个区间内全部为 \(1\),即区间最大值为 \(1\),则没有必要递归。

这样的话每个点被遍历到并被修改的次数最多 \(O(n\log V)\),时间复杂度即 \(O(n\log n + n\log V)\)

这种性质主要表现为有效操作次数有限,不仅局限于值的变化,也可以是值的种类。

给定一个长为 \(n\) 的数组 \(a\),进行 \(q\) 次以下两个操作:

  • 给定 \(l, r, k\),将 \(a\) 数组区间 \(l, r\) 内的所有 \(> k\) 的数赋值为 \(k\)
  • 给定 \(l, r\),查询 \(a\) 数组区间 \(l, r\) 的所有值的和。

区间取 min,区间求和。显然无法使用正常的线段树维护,因为 \(tag\) 无法和区间和这个信息合并。

但容易发现区间取 min 如果是小于区间次大值,那么区间中值的种类一定减少了。(区间 min 得到的 \(k\) 可能会使区间种类数 + 1)

所以我们维护区间最大值,次大值,最大值出现次数。如果取min的值大于最大值,则忽略,大于次大值,可以通过最大值次数和最大值求出修改后区间和,打上懒标记。如果小于次大值,则暴力更新子树。

暴力更新子树的时候区间的值种类必然多于 \(2\),并且在此类操作之后种类会减少至少 \(1\)

初始时区间种类为 \(O(n)\),每次取 min 操作最多多出 \(O(\log n)\) 个种类,所以我们最多只会进行 \(O(n + m\log n)\) 次暴力更新,时间复杂度是对的。

posted on 2023-11-10 16:20  Evan_song  阅读(57)  评论(0)    收藏  举报