主席树的简要讲解

区间第k小值

主席树是解决动态查找序列上[l,r]区间中的第k小值的一个数据结构

核心思想:动态开点(后面会讲)
传统线段树都是值域线段树
其实意思就是每个节点都存的是序列上[l,r]的一个区间和,但是考虑我们需要动态处理区间的不是最值,故换一种线段树

主席树一般用的是权值线段树
就是把[l,r]改为[min(a),max(a)] a是序列A[l,r]区间里的一个子序列,每个节点下都保存一个统计数字s,代表在当前权值域里,有多少个数字
array : 1 4 3 3

           [1,4]
           s = 4
    [1,2]         [3,4]
    s = 1         s = 3
[1,1]  [2,2]    [3,3] [4,4]
s = 1  s = 0    s = 2  s = 1

 

 (不会画图QWQ)

 步入正题
我们转换思想,每一次开点都存储一个版本的权值线段树,但是这样会让空间复杂度直线上升为O(n^2),这是不能容忍的
但是我们再想
这是一颗树性结构,我们每一次加点都只会改变上下log n个点的数据,为什么我们就不能每一次都开一个log n的新点,将与这次更改无关的点直接开一条新边连接到新点呢?

不能像上一个那样形似了,我直接盗图

这是第一个版本在insert(4)后的结果
每一次insert都存储一个root[i+1],表示第i+1个版本的根节点

 

但是如何查询[l,r]区间内的第k小数呢
这时候就要运用我们前缀和的思想


 [l,r]的信息 = [1,r]的信息 - [1,l-1]的信息
即 S[l,r] = S[1,r] - S[1,l-1]

离散化
这个其实没什么好讲的,就是预处理,详情见代码

 

大概意思是懂了吧,来,让我们步入总结

常规操作
1.插入insert(),动态开点
2.查询[l,r]区间内第k小值
3.离散化

时间复杂度 O(n log^2 n)

luogu P3834
#### C++ 代码

 1 #include"bits/stdc++.h"
 2 
 3 using namespace std;
 4 const int N=200010;
 5 #define inl inline
 6 #define reg register
 7 #define regi register int
 8 #define PII pair<int,int>
 9 //#define ll long long
10 inl int read(void)
11 {
12     int x=0,f=1;char ch=getchar();
13     while(!isdigit(ch))  f=ch=='-'?-1:f,ch=getchar();
14     while(isdigit(ch))   x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
15     return x*f;
16 }
17 int a[N],n,m;
18 vector<int>    num;
19 struct node
20 {
21     int l,r,s;//左右儿子下标,区间中一共有多少个数
22 }tr[N*20];
23 int root[N],idx;//root[i]为第i课线段树的节点编号
24 #define id(x)    lower_bound(num.begin(),num.end(),x)-num.begin()
25 //id(x)为映射
26 int build(int l,int r)
27 {
28     int p=++idx;
29     if(l==r)    return p;
30     int mid=l+r>>1;
31     tr[p].l=build(l,mid),tr[p].r=build(mid+1,r);
32     return p;//返回当前节点的编号
33 }
34 inl int insert(int p,int l,int r,int x)
35 {
36     int q=++idx;//创建新点q
37     tr[q]=tr[p];//复制旧点p 
38     if(l==r)
39     {
40         tr[q].s++;
41         return q;
42     }
43     int mid=l+r>>1;
44     if(x<=mid)    tr[q].l=insert(tr[p].l,l,mid,x);//x出现在左子树,修改左子树
45     else    tr[q].r=insert(tr[p].r,mid+1,r,x);//x出现在右子树,修改右子树
46     tr[q].s=tr[tr[q].l].s+tr[tr[q].r].s;//统计
47     return q;//返回当前分配使用的新节点编号
48 }
49 inl int query(int q,int p,int l,int r,int k)//查询区间
50 {
51     if(l==r)    return l;
52     int s=tr[tr[q].l].s-tr[tr[p].l].s;//线段树相减
53     int mid=l+r>>1;
54     if(k<=s)    return query(tr[q].l,tr[p].l,l,mid,k);//左儿子数字大于或等于k时,说明第k小数在右子树
55     return query(tr[q].r,tr[p].r,mid+1,r,k-s);//否则在左子树
56 }
57 void init()
58 {
59     //离散化
60     sort(num.begin(),num.end());
61     num.erase(unique(num.begin(),num.end()),num.end());
62     root[0]=build(0,num.size()-1);
63     
64 }
65 int main(void)
66 {
67     n=read(),m=read();
68     for(regi i=1;i<=n;i++)
69     {
70         a[i]=read();
71         num.push_back(a[i]);
72     }
73     init();
74     for(regi i=1;i<=n;i++)    root[i]=insert(root[i-1],0,num.size()-1,id(a[i]));
75     while(m--)
76     {
77         int l=read(),r=read(),k=read();
78         printf("%d\n",num[query(root[r],root[l-1],0,num.size()-1,k)]);
79     }
80     return 0;
81 }

可持续化的思想都基本是这样的,包括可持续化平衡树,可持续化堆(注意是思想,比如可持续化堆有的时候就不能动态开点)

 

热门相关:末日乐园   美漫之大冬兵   美漫大幻想   我的女友是丧尸   灭世之门