可持久化数据结构<静态可持久化线段树>

可持久化数据结构<静态可持久化线段树>

可持久化数据结构

数据结构的可持久化的定义:在一次操作(增删改查)后,保留操作前的版本信息

这里最无脑的办法是一次操作(增删改查)前对其进行深拷贝,这样便保留了历史版本,但是这么做来保留历史版本就会面临问题是夹杂着大量重复数据(数据库里叫”脏数据”),所有可持久化数据结构都是围绕如何减少脏数据来实现的,可持久化数据结构的核心就是如何保留历史版本和减少脏数据。

 

什么样的数据结构能可持久化?一个数据结构能可持久化要满足:

  1. 保留一次历史版本所占用的空间复杂度和时间复杂度都小于等于O(logn),
  2. 最坏空间复杂度和时间复杂度都小于等于O(logn),所以均摊时间复杂度为log(n)但最坏为O(n)的伸展树splay不能可持久化,原因在于这样做保留多次历史版本后,那些最坏情况的出现会被”放大”,拉低效率。

 

可持久化数据结构最好使用伪指针索引,因为乘法索引在森林行不通,而指针索引的对于频繁插入删除效率不行,而且考虑操作系统层的寻址问题,伪指针索引的内存连续,显然更快。

对于查询操作,如果查询后数据结构不发生变化,则不必对这个操作可持久化,但插入和删除操作一般是要可持久化的

静态可持久化线段树:

区间第k小问题:

给定序列A[0]到A[n-1] 和m次询问 (1<n<1e5,abs(A[i]) <1e9 )

每次询问区间[l,r]内第k小的数是多少?

 

我们知道,求序列整体的第k小有2种常用方法,一个是平衡树,一个是权值线段树,具体原理参考数据结构的其他文章。在此权值线段树的基础上强化就诞生了可持久化权值线段树。

 

可持久化权值线段树 :简称主席树,可持久化线段树,函数式线段树

是基于权值线段树的数据结构,权值线段树处理范围较大的离散数据需要离线做(大于1e6),主席树既然基于权值线段树,也是一种离线数据结构。

 

先对所有数据排序 (指A[i]),用数组建立 <顺序,数值> 的索引,已知顺序索引数值直接用数组下标即可。若是已知数值去索引顺序,用二分查找或者哈希表预处理都可以。得到不同的数字的个数设为up,用up建树维护区间[0,up)。

属性:

主席树是复合森林,是很多棵二叉树的复合体,直观的印象如下图 (有2个根时候的样子):图来自洛谷

需要注意的是,普通线段树一般有三种索引方式,乘法索引和指针索引和拟指针索引。

主席树不能像线段树那样,通过计算(now*2和now*2+1)得到左右儿子,只能像其他普通二叉树那样,指针索引和拟指针索引实现。再有就是主席树空间很大,线段树的非必要信息,如左右端点信息,尽量就省略不维护了。

 

树的节点属性有:son [2]代表子树位置,sum代表区间和

此外还要维护root[]数组,root[i]代表第i个根的下标。如果用拟指针索引,还要定义节点集合数组:tree[],实际证明,拟指针索引比指针索引要快一些。

 

这种用数组维护历史版本线段树的根的数据结构,叫静态可持久化线段树

实际上静态主席树的代码并不多,但静态主席树不支持修改操作(看作可持久化数组时,可以修改)

建树:

利用线段树的建树方式,建立维护区间[0, up)的树,这颗树是空的sum=0,到这里还和权值线段树差不多。不要忘给root[0]赋值代表空树树根是root[0]

我们开始把每个A[i]插入刚才建的树里。

插入:

就是类似线段树单点更新,但是不去修改节点,而是建新的节点。

对于插入一个A[i],得到离散化后的数值x=ind[A[i]],遍历上一棵树root[last],主席树里插入x,类似权值线段树插入x,看mid和x的大小关系,x<mid向左,反之向右,递归过程不修改节点,而是每层添加新节点,建立方式参考图中,相当于一次多出了logn个节点。

这里要注意从,每次插入操作,会建立新root,要记录下来。

 

单点历史版本查询/可持久化数组

对于查询第i各版本的第j个位置的值,就从root[i]开始走,类似二分查找那样,如果mid>=j向左,mid<j向右,时间复杂度O(logn)

  1. 当作可持久化数组用时,是在线算法,叶子节点储存数据,其他节点的数据无意义。
  2. 此外,网络上对于应用单点查询和修改的可持久化权值线段树,还称为可持久化数组,原本静态主席树不支持修改,但在这里我们只维护叶子节点,非叶子节点属性无意义,所以当作线段树那样修改只需要修改一个值,所以当作可持久化数组时,单点修改有意义。

查询区间第k小:

对于查询区间第k小问题,先把数据离散化,使得他们紧凑。

然后建立一个空的主席树,每个节点sum是0

对于插入操作,用主席树的插入操作去插入,插入的第i个数A[i]对应的那个根节点的树,就代表了前i个数所建立的权值线段树。我们能利用这些权值线段树得到:前i个数的第k小(权值线段树查询总体第k小参考其他文章)。

 

对于查找前n个数的区间[x,y]第k小(x和y从1开始)

可以通过查询前y个数第k小和前x个数第k小得到。

方法:我们分别从root[x-1]和root[y]这两棵树的查找,对于各自每次走的节点,

设它们左儿子的sum属性的差值是d:

如果d>=k, 说明被找的数在左边,同时往左儿子找第k小

如果d<k,  说明被找的数在右边,同时往右儿子找第k-x小,

且我们的视线是同时从root[x-1]和root[y]下降的,要找到根节点一定同时到根节点,如果到了根节点,则返回这个节点的左端点值即可。

 

实际上寻找区间第k小的原理,就是利用不同历史版本之间的差异性,来由普通权值线段树的查询总体第k小功能扩展出区间第k小的

代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 1e-5
#define rg register
#define cint const int &
#define cll const long long &
#define cdou const double &
#define cv2 const v2 &
const int inv2=500000004;
const int INF=2147483647;2139062143
const int MAX=200010;
const int mod=1e9+7;
/*
未在节点保存端点,而是放在递归函数里(记作tl,tr)来节省空间
*/
struct Node{
    int sum,son[2];
};
struct PersistentLineTree{
    int size,rlen,up;
    int root[MAX];//表示根节点集合,root[0]代表最原始的树
    Node tree[MAX<<5];
    void build(int l,int r){
        size=rlen=1;
        root[0]=__build(l,r);
    }
    int __build(int l,int r){
        int now=size++,mid=(l+r)/2;
        tree[now].sum=0;
        if(r-l>1){
            tree[now].son[0]=__build(l,mid);
            tree[now].son[1]=__build(mid, r);
        }
        return now;
    }
    void insert(int tl,int tr,int x){//单点修改,在最新版本基础上x位置加1,x是离散化之后的数值
        int last=root[rlen-1];
        root[rlen++]=__insert(last,tl,tr,x);
    }
    int __insert(int other,int tl,int tr, int x){//单点修改,在other版本的树的基础上的x位置加1,x是离散化之后的数值
        int now=size++;
        Node &t=tree[now];//t去引用tree[now],以后t就是tree[now]
        t=tree[other];//把历史版本的一个复制给tree[now]
        t.sum++;
        if(tr-tl>1){
            int mid=(tl+tr)/2;
            if(x<mid){//节点编号在左边,往右找
                t.son[0]=__insert(tree[other].son[0],tl,mid,x);
            }else{//节点编号在右边,往右找
                t.son[1]=__insert(tree[other].son[1],mid,tr,x);
            }
        }
        return now;
    }
    int searchKi(int l,int r,int tl,int tr,int k){//查询区间[l,r]的第k小
        return __searchKi(root[l-1],root[r],tl,tr,k);
    }
    int __searchKi(int root1,int root2,int tl,int tr,int k){//查询区间第k小,用法见文档
        if (tr-tl<=1)//是叶子结点
            return tl;
        int tl1=tree[root1].son[0];
        int tl2=tree[root2].son[0];
        int x=tree[tl2].sum - tree[tl1].sum;
        int mid=(tl+tr)/2;
        if(x>=k)
            return __searchKi(tl1,tl2,tl,mid,k);
        else
            return __searchKi(tree[root1].son[1],tree[root2].son[1],mid,tr,k-x);
    }
};
PersistentLineTree tr;
struct v2{
    int x,y;
};
v2 A[MAX];
int disA[MAX];//离散化之后的原数组
int discrete(v2 *A,int *disA,int n){//离散化
    sort(A,A+n,[](const v2 &a,const v2 &b){
         return a.x<b.x;
    });
    int k=0;
    disA[A[0].y]=k;
    for(int i=1;i<n;i++){
        k+=A[i].x!=A[i-1].x;
        disA[A[i].y]=k;
        A[k]=A[i];
    }
    return k+1;
}
int main(){
	int i,l,r,k,n,m,up,ans;
	scanf("%d%d",&n,&m);
	for(i=0;i<n;i++){
        scanf("%d",&A[i].x);
        A[i].y=i;
	}
    tr.up=discrete(A,disA,n);
    tr.build(0,tr.up);
    for(i=0;i<n;i++){
        tr.insert(0,up,disA[i]);
    }
    for(i=0;i<m;i++){
        scanf("%d%d%d",&l,&r,&k);//l和r从1开始
        ans=tr.searchKi(l,r,0,up,k);
        printf("%d\n",A[ans].x);
    }
return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值