题解 P3834 【【模板】可持久化线段树 1(主席树)】
瑞源鸣
2019-03-01 22:16:04
今天刚学完主席树,~~个人觉得还是比较简单的~~
------------
- ### **先来波热身题**
```
依次给定n个数,再给定m个询问,问前i个数中第k大的数是多少,保证每次询问的位置递增。
输入:
5 3
1 2 3 4 5
2 2
4 2
5 3
输出:
1
3
3
```
------------
- ### 我们来分析一波:
如果学过平衡树,我们容易想到用平衡树解决,但事实上不需要。只需要用一个 ~~基础~~ 的数据结构—— **线段树** !?
如果没学过也无所谓,这有个大佬的平衡树的参考 **[博客](https://www.luogu.org/blog/teljian/solution-p3369)** ,不懂的也可以问我,~~就顺便学了吧~~
我们可以用 **权值线段树** 解决这个问题:
1. 先建一颗有n个节点的空树
1. 将 **编号** 作为 **权值** 存放进线段树,每个节点表示当前节点有多少棵 **子树**
1. 查询的时候可以直接采取 **线段树** 的查询方式
![](https://cdn.luogu.com.cn/upload/pic/52980.png)
此时我们可以比较当前的 **k** 与根节点的 **左儿子的size** 比较,若小于左儿子,则往左儿子找,否则往右儿子找。找到叶子节点后输出对应的值即可。
##### ~~代码略(这不是关键)~~
------------
- ## **回归这道题,是否觉得有一丢丢思路了**
1. 我们先建立n棵权值线段树
1. 然后只需将第R棵线段树减去第(L-1)棵线段树,再跑一次热身题的那种方法即可
- ### 若查询区间 [2,4]找第2小的数:
![](https://cdn.luogu.com.cn/upload/pic/52981.png)
1. 然后由于2>左儿子的1,所以往右儿子找,此时k更新为2-1=1。
1. 再找下一层,由于1=1,所以往左儿子。找到根节点,返回对应的值。
------------
- ### 问题来了!!!
此时时间复杂度和空间复杂度都是O(n^2)! ! ! !
接下来就引出我们重要的主席树的 **核心** 了:
### 我们会发现,相邻的两棵树只有一条"链"不相同
所以我们没有必要再开一颗完整的线段树,只需要在上一棵树的基础上重新增加一条链就可以了,剩下的不变的地方就沿用以前的就可以了。~~简单的思维!~~
#### 注意一下,由于数的范围比较大,所以我们需要离散化。因为要知道原来的值,所以开双向映射。同时需要一个root数组记录不同的树的根节点。
![](https://cdn.luogu.com.cn/upload/pic/52985.png)
------------
- # code
(码风丑,勿喷)
```cpp
#include<cstdio>
#include<algorithm>
using namespace std;
const int M=(2e5)+10;
#define ls tree[now].l
#define rs tree[now].r
int n,m;
int k,N;
int root[M];
int a[M],ys[M];
//a存离散化后的数,ys建立离散化后与原数的映射关系,root为森林的不同根节点
struct Node{
int num;
int id;
}q[M];
struct node{
int l,r;
int size;
}tree[M<<5];//多开 1<<5 的空间
inline int read(){
int f=1,x=0;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
bool cmp(Node x,Node y){//求第k小的数,所以从小到大排序
return x.num<y.num;
}
void lsh(){//离散化模板
a[q[1].id]=++N;
ys[N]=q[1].num;//注意存映射的点
for(int i=2;i<=n;i++){
if(q[i].num!=q[i-1].num) N++;
a[q[i].id]=N;
ys[N]=q[i].num;
}
}
int build(int l,int r){//动态建树,防止数组越界或开不够
int now=++k;//加点
if(l<r){
int mid=(l+r)>>1;
ls=build(l,mid);//更新左儿子
rs=build(mid+1,r);//更新右儿子
}
return now;
}
int update(int pre,int l,int r,int x){//建权值线段树
int now=++k;
tree[now].size=tree[pre].size+1;//此线段树的点数为上一棵的点数+1
ls=tree[pre].l;
rs=tree[pre].r;
//继承上一棵线段树
if(l<r){//寻找需要更新的链
int mid=(l+r)>>1;
if(x>mid) rs=update(rs,mid+1,r,x);//在右儿子更新
else ls=update(ls,l,mid,x);//在左儿子更新
}
return now;
}
int query(int u,int v,int l,int r,int x){//查询操作
if(l==r) return l;//找到第k小值
//第r棵线段树左儿子-第(l-1)棵线段树左儿子的值
int t=tree[tree[v].l].size-tree[tree[u].l].size;
int mid=(l+r)>>1;
if(x<=t) return query(tree[u].l,tree[v].l,l,mid,x);//在左儿子上
else return query(tree[u].r,tree[v].r,mid+1,r,x-t);//在右儿子上
}
int main(){
n=read();m=read();
for(int i=1;i<=n;i++){
q[i].num=read();
q[i].id=i;
}
sort(q+1,q+n+1,cmp);
lsh();
root[0]=build(1,N);//建一颗空的线段树
for(int i=1;i<=n;i++)//建n棵线段树,边加点边建树
root[i]=update(root[i-1],1,N,a[i]);
for(int i=1,l,r,x;i<=m;i++){
l=read();r=read();x=read();
printf("%d\n",ys[query(root[l-1],root[r],1,N,x)]);
//[l,r]就等价于 第r棵线段树-第(l-1)棵线段树 的k小值,返回该节点映射的值
}
return 0;
}
```