主席树【可持久化线段树/函数式线段树/n棵线段树】
目录
一. 概念理解
- 离线数据结构
- 可以查询区间第k大
- 复杂度log(n)
1.求所有数字中的第k大数?
先将所有数字离散化【排序+去重】(所以是离线)。
对离散化后的数字建立一颗线段树,每个节点【 统计当前范围内的数的个数 】 。
自顶向下查找,如果左边区间个数大于等于k则在左边,小于则在右边(从大到小排序)。
2.求任意一段区间 [ L , r ]中的第k大数?
建立n棵线段树,每棵维护 [ 1 , i ] 的数字出现情况。
则 [ L , R ] = [ 1 , R ] - [ 1 , L - 1 ] (前缀和思想)。
3.会不会空间超限?
n棵线段树,每棵2n个点,怎么也是n^2的空间?
优化方法:注意到每棵新树都转移到上一棵树,只是改了一条从某叶子到根节点的路径 。
那么除了这条路径,其他的都可以直接从上一棵树上转移过来
大概就长这样了 ↓↓↓ 总共只用维护一棵树。空间复杂度O(nlogn)。
( 这个添加过程可以看做是:“链ys”加入“初始树xs”,即加入第一个元素 f 的过程 )
(d',g',f'都是新树相对于原树有修改的点,增加结点编号,把新旧结点都储存下来 )
当然,看到这里还是难以理解(反正本蒟蒻是没懂qaq)
那就让本蒟蒻手动模拟一下试试吧...(像素渣&&字渣无误)
- 以序列 4 1 3 2 为例,查询区间 [ 2,4 ] 中第3小的数。
用num[ ]记录原序列。首先“离散化+排序判重”,得到排序后的s数组 1 2 3 4 。
先确定初始线段树状态,再按照num[ ]的顺序,将 4 1 3 2 依次存入线段树中。
ONE POINT{很重要的理解}:下图圆圈内的数就是线段树存的权值,代表着:
统计(按照排名记录的区间)区间 [ i , j ] 中,已经存入的原序列的数(num[ ])的个数。
底层叶子节点记录的是,这个排名位置的数是否已经在树中建立。
0代表还没有建立,1表示已经建立,同时关联的上方管理数组的权值也会相应改变。
下面来模拟建树过程——
(状态1)原始空线段树,所有点的权值都为零。
(状态2)插入num[1]=4,到排序后它应该在的4号位置(是第四小的数)。
第一棵修改树可以用根节点序号标记为root[1]=8(具体root的实现看后面)。
(状态3)插入num[2]=1,到排序后它应该在的1号位置(是第一小的数)。
(状态4)插入num[3]=3,到排序后它应该在的3号位置(是第三小的数)。
(状态5)插入num[4]=2,到排序后它应该在的2号位置(是第二小的数)。
这样,未优化空间的、插入元素使用的、5棵树就建好了。
为优化空间,可以使用树的合并(就是上面第三大点说的【优化空间复杂度】)。
主席树的两个主要函数:插入和合并。
插入:动态开点。很多时候线段树维护的区间很大,而能定义的空间是有限的,
所以我们就只给那些有用的点一个编号就可以了(只有在修改时被访问过的点才是有用的)。
虽然这样的线段树是残缺不全的,但也还是线段树。
合并:当两棵线段树的下标和维护的范围大小都一样,这两棵线段树就可以合并。
(null表示还没有存入的节点,圆圈中的数表示存储的下方的已插入结点的个数)
(图中最下方的数字指的是离散化后的排名情况...)
对于每个位置都建一颗线段树,其实是一条链。
然后再按照顺序把线段树都合并起来。第 i 棵线段树维护的就是 1~i 区间的信息。
通过合并可以知道,线段树满足可加性,那么得到的信息肯定可以相减。(↓前缀和思想)
对于每一次询问通过第 r 棵线段树和第 ( l - 1 ) 棵线段树作差,即可得到该区间的信息。
因为我们是以离散数组构建的主席树,那么从根节点出发,左子树部分的数必定不大于右子树部分的数。
于是就可以将左儿子的节点个数 x 与 k 做比较,若 k≤x,则第 k 小值一定在左子树里面,
若 x≤k,则第 k 小值一定在右子树里面,然后递归往下走,缩小范围。
注意,前者递归时, k 直接传下去即可,后者递归时,需要将 k 减去左子树的数的个数再传递这个 k 值。
例如我们查找 [1,4] 中第 2 小的值,图示如下,绿色节点为该值存在的区间位置。
↑↑↑ 需要注意的是,第二个绿色节点才是绿色根节点的左子树,因为左子树表示的区间是靠前的那一半。
方法步骤总结:
现在我们真正来解决区间询问 [ l , r ] 的问题。
解决方案就是将主席树 [1,r] 减去主席树 [1,l−1] 。
首先看到主席树的底层,全部是对数字个数的统计。