主席树入门

主席树入门

1.引入

经典问题:静态区间第k小问题

问题大意如下,给出n个数,询问任意区间第k小的数是多少,

解决思路:

思路1:使用暴力,把查询区间数字排序,寻找第k个数

思路2:拆解问题,首先考虑给定区间第k小的数怎么求,再考虑如何取任意区间的数

2.权值线段树

按照思路2的第一步,引入一种线段树,叫做权值线段树(是线段树的一种),目的是求固定区间的第k小。

普通的线段树维护一个区间的属性,该树维护权值的属性。

在本题里,我们规定节点的属性size表示在该段范围中数据个数。

需要注意的是:维护的是权值,所以每个节点的范围为数据范围,有时需要配合离散化

如图,若输入数据为4 4 2 3 1 3 ,则线段树应为,举例:1~2范围内的数据有:1、2两个,Size为2

其中我们可以发现权值线段树的一个特性:就是查询复杂度为O(logn)

权值线段树的特点列举:1.深度约为logn   2.叶节点的范围为一个数据大小,无左子右子   3.左子范围必定小于右子的范围   4.节点Size值=左子Size值+右子Size值   

查询操作和平衡树差不多,主要利用左右分叉的特点。

若我想查询第k小的数据,每次只需要比较左右子的Size与k的大小,用来缩小第k小数的范围若k>左子Size,则在第k小在右子范围,反之同理。知道下一步往左走或往右走,每走一步,就会缩小一次范围,由于是二叉树,所以范围缩小非常快,直到到叶节点就搜到了(范围为一个数,确定)。需要注意的是,走右子时k需要减去原左子Size,因为右子范围的最左值会变化,原区间[l,r]的第k小实际转变为新区间[a,r]的第(k-原左子Size)小。

如图,若想查询第3小的数据,则只要从根节点①开始查找。我们发现,左子的Size为2则第3小的数必定在右子树的范围中,我们走到⑤时,求[1~4]范围的第3小实际上变为求[3~4]的第1小,即减去了②的Size,最终走到了⑥,发现范围为一个数的大小,确定该数为3,旅程结束。

3.时间复杂度升级

如上可实现固定区间内的第k小查询

那么我们如何升级呢?我们知道,线段树的升级是很微妙的,如懒标记,懒标记是为了适应线段树updata操作的遍历问题,降低时间复杂度所用。

我们应从前缀和的方式入手。

 

 我们从数据输入开始建树,给建n颗树。

第i棵树维护数据1~i的Size值,由于总的数据范围给定,所以每一颗线段树的规模应是一样的。

因为对于第i棵树与第j颗树(i<j) 第j颗树维护的数据是(1~i的数据)+(i+1~j)的数据,第i棵树维护的是(1~i)的数据,所以第j棵树的每个Size减去第(i-1)颗树的每个Size为i~j范围的Size值。

由于比较讲不来,还是画图了,数据还是老数据:4 4 2 3 1 3

 

搜索新树,就可以知道[4~6]区间的第k小值。

其他也同理,即搜索第j棵树-第i棵树得到的新树代表[i+1,j]范围的Size值。

操作也很简答,因为每颗规模一样,同时搜索两棵树,边搜边减即可视为搜索新树。

 

 4. 再优化

由于要开n个这样的树,是个人都受不了。

我们就思考,一定要开一个新树吗?

结果你发现,其实对于相邻的两棵树i与i+1,第i+1颗树只比第i棵树多了一个数据,因为线段树的特性:只有一列的结点的范围包含该数据,所以只有一列的Size是不一样的。只要给每颗新树预留一列的新空间,空间复杂度就下来了。

建立的过程也很简单,一般我们先建一颗空树(图片是先建一颗空树),或者把第一个数据当树。

老数据4 4 2 3 1 3,先建空树

 

如图依次插入数据

 

 

每新插入一个数据就借用前一棵树的一部分+自己的一列。

我们分析:由于查询从一个根结点开始,边是单向的,所以每棵树的查询互不影响

举个例子,实际上的第二棵树为橙色部分所示

实际上的第3棵树为绿色部分所示

 

 具体操作就是,如果自己的左子Size需要增加,就把上一颗树的右子当作自己的右子,新建左子,反之同理

 

 

 5.总

查询[l,r]区间时,同时搜索第r棵树和第l-1颗树,每一颗都把Size的差值求出,与k的大小进行比较,走左子或者右子,搜寻到叶节点就可以寻找到第k小。

主席树是一种可持久化线段树,他的特点在于对于历史操作有极高的保留性,利用原线段树后续操作可空间压缩的特点,能完成许多“历史操作”,其难点不在于码字,而在于如何应用。

 

posted @ 2021-06-30 18:16  QueenSi  阅读(143)  评论(0)    收藏  举报