线段树

普通线段树

线段树是一个二叉树,一般用于维护数组。每个节点维护其子树所包含的区间的信息,父亲节点的信息可由左右儿子得到。

典中典的线段树维护的是单修区查,单点修改复杂度 \(O(\log n)\),因为包含一个点的区间不超过 \(O(\log n)\)。区间查询也是 \(O(\log n)\),因为一个区间分割出来的区间不超过 \(O(\log n)\)

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

线段树于是可以在 \(O(\log n)\) 的时间内进行操作,但其维护的内容必须满足以下条件:

  • 可合并性 - 可以将左右儿子的信息合并到父亲,例如区间求和,区间求 max。而区间 mex 则不行。区间最大子段和难以直接维护,但可以通过维护前缀max和后缀max得到。

如果要支持区修的话还需要加一个条件:

  • 可标记性 - 对于一个修改操作,需要打下懒标记来减少子树的访问,打懒标记时我们需要能够预知当前区间的值。例如区间加时可以计算出修改后当前区间的和。但区间取min时不行。

线段树的时间复杂度是 \(O(\log n)\)

值域线段树

值域线段树用线段树的方式维护值域数组。可以用于查询值属于 \([l, r]\) 的元素个数。

由于值域一般较大,在建值域线段树时会使用动态开点。

动态开点

一般线段树使用左儿子下表为父亲的两边,右儿子下标为左儿子加一的方法保存数据。动态开点则动态分配空间,需求储存时才开出新的点。

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

适用于数据范围大,但有效信息为 \(O(n)\) 的情况。例如数组初始为空,这样每次操作最多增加 \(O(\log n)\) 个节点。

标记永久化

打懒标记的另一种方式,即标记不进行下传,而是在查询时再对答案进行修正。

一般在可持久化线段树上会使用,由于可持久化线段树一个节点可能属于多个时间状态的线段树下,所以不能进行直接修改,需要使用标记永久化。

可持久化

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

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

一个比较经典的题就是区间 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)\)

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

区间取 min,区间求和。容易发现区间取 min 如果是小于区间次大值,那么区间中值的种类一定减少了。

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

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

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

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