可持久化线段树(主席树)

如果有m次修改,就会存在m+1个版本
如果某一个结点的信息发生的变化,就会创造一个全新的结点出来

struct{
  int l,r;//表示左右子节点的下标,而不是表示其区间的左右端点
  int cnt;//当前区间中一共有多少个数 
}; 

可持久化线段树难以进行区间修改的操作。因为它难以处理懒标记,因为有很多版本。
主席树中每个版本的结点个数一样,所以比较容易一点。
若是修改了一条路径,就创建条修改后的路径。

 

修改了左边红色路径上的信息,那么就重新创建右边这样路径的修改后的信息。红色的横线连接的两个相同意义的点。绿色的线连接的是修改后的点和原先并未修改的子结点

按照数值而不是下标上建立线段树,维护每个数值区间中一共有多少个数
如何求整体的第k小数:mid=1+n>>1,如果[1,mid]之间的数的个数cnt>=k,那么就递归左边,否则递归右边
如何求[1,R](原序列的下标)中的第k小数:在可持久化线段树(从左往右,给出一个点就更新一个版本)中只需要找到第R个版本,然后搜这个版本的第k小数就可以了
如和求[L,R](原序列的下标)中的第k小数:
在第R个版本中[l,r](数值)的个数是cnt1,在l-1版本中[l,r](数值)的个数是cnt2.cnt1-cnt2就是[L,R](原序列区间)中[l,r](数值)中的数的个数

一个典型的例题:https://www.acwing.com/problem/content/description/257/

题目描述

给定长度为N的整数序列A,下标为 1N

现在要执行M次操作,其中第i次操作为给出三个整数li,ri,ki,求A[li],A[li+1],…,A[ri][(即A的下标区间[li,ri中]第ki小的数是多少。

输入格式

第一行包含两个整数N和M。

第二行包含N个整数,表示整数序列A。

接下来M行,每行包含三个整数li,ri,ki用以描述第i次操作。

输出格式

对于每次操作输出一个结果,表示在该次操作中,第k小的数的数值。

每个结果占一行。

数据范围

N1e5,M1e4,|A[i]|1e9

AC代码(主席树模板代码):

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10,M=1e4+10;
struct node{
    int l,r;//表示左右子节点的空间下标,而不是表示其区间的左右端点
    int cnt;//表示该数值区间中有多少个数 
}tr[4*N+17*N];//线段树第0版本的空间+N(修改次数、版本空间)*logn

int root[N],id=0,a[N];//root每个版本的头指针;id每增加一个结点的空间下标 
vector<int>all;

void pushup(int u){//更新结点信息 
    tr[u].cnt=tr[tr[u].l].cnt+tr[tr[u].r].cnt;
}

int build(int l,int r){//创建一颗树。也就是最初版本的线段树。l,r是离散化后的数值的下标 
    int p=id++;//id是还未使用的数组的第一个下标,创建的结点的空间下标就是此下标 
    if(l==r) {//当l==r时,表示到达了叶子节点 
        tr[p].cnt=0;
        return p;
    }
    int mid=l+r>>1;
    tr[p].l=build(l,mid);//左右递归创建线段树 
    tr[p].r=build(mid+1,r);
    return p;
} 

int find(int x){//二分搜索,查找离散化后的下标 
    return lower_bound(all.begin(),all.end(),x)-all.begin();
}

int insert(int p,int l,int r,int x){//插入一个数值,也就是更新一个版本。p是上一个版本对应的下标 
    int q=id++;
    tr[q]=tr[p];//首先将上一个版本的对应结点的信息复制过来 
    if(l==r){//说明到达了需要插入的结点的位置 
        tr[q].cnt++;
        return q;
    }
    int mid=l+r>>1;//二分插入 
    if(x<=mid) tr[q].l=insert(tr[p].l,l,mid,x);
    else tr[q].r=insert(tr[p].r,mid+1,r,x);
    pushup(q);//向上更新 
    return q;
}

int query(int q,int p,int l,int r,int k){
    if(l==r) return l;//表明到达了要查询的第k小结点 
    int cnt=tr[tr[q].l].cnt-tr[tr[p].l].cnt;//判断在[L,R]区间内有多少给定的区间内的数 
    int mid=l+r>>1;//二分 
    if(k>cnt) return query(tr[q].r,tr[p].r,mid+1,r,k-cnt);//递归查询 
    else return query(tr[q].l,tr[p].l,l,mid,k); 
}

int main(){
    int n,m;scanf("%d%d",&n,&m);
    for(int i=0;i<n;i++){
        scanf("%d",&a[i]);
        all.push_back(a[i]);
    }
    //将数值进行离散化 
    sort(all.begin(),all.end());
    all.erase(unique(all.begin(),all.end()),all.end());
    root[0]=build(0,all.size()-1);//0,all.size()-1是数值端点离散化之后的左右端点
    for(int i=0;i<n;i++)
        root[i+1]=insert(root[i],0,all.size()-1,find(a[i]));//创建多个版本 
    int l,r,k;
    while(m--){
        scanf("%d%d%d",&l,&r,&k);
        cout<<all[query(root[r],root[l-1],0,all.size()-1,k)]<<endl;
    }
    return 0;
}
 

写于:2020/9/1 19:23


作者:孙建钊
出处:http://www.cnblogs.com/sunjianzhao/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文地址:https://www.cnblogs.com/sunjianzhao/p/13598058.html