主席树模板&可持久化数据结构介绍

引言

一、

可持久化数据结构的前提是其拓扑排序是不变的

比如线段树,我们改变的总是它维护的数值,他的结构一旦定下来就不会变了;

二、

可持久化的核心思想是类似git操作,我们维护版本间的差异,而不是维护整个版本


主席树

主席树又称可持久化线段树,其持久化的核心思想和引言的一致;

主席树难以进行区间修改操作(当然我们可以通过永久化懒标记实现, 一般碰不上)


也就是我们每修改一个节点,我们就创建一个新的副本,并在副本的基础上进行修改;

在这里插入图片描述
如图所示,假设我们修改了 1 , 2 , 4 , 8 1,2,4,8 1,2,4,8这条链,我们就创建这条链的一个备份,并且在这条链上修改;


每个版本我们都认为他是一颗线段树

因为版本间只有若干节点的不同

因此其他相同节点是各个版本共用的;

空间大小

因为线段树上的修改,每次只会涉及 4 l o g 2 n 4log_2n 4log2n左右的节点;

假设有 n n n个节点,一般来说空间只需要开 4 ∗ n + n l o g 2 n 4*n+nlog_2n 4n+nlog2n就够了;

如果要保险的话,开 n < < 5 n<<5 n<<5肯定够了…

例题一

第K小数

在这里插入图片描述
在这里插入图片描述

思路

首先,因为数值比较大,范围比较小,因此我们离散化处理一下;

其次,我们在数值上建立一颗线段树(注意不是区间);

然后遍历数组的每一个元素,每遍历一个元素,我们就创建一个新的版本

这样我们一共有 n + 1 n+1 n+1个版本(0~n);

我们用 r o o t ( i ) root(i) root(i)表示第 i i i个版本的线段树根节点编号;


询问 [ L , R ] [L,R] [L,R]时,我们用第 R R R个版本的线段树减去 L − 1 L-1 L1个版本的线段树;

因为第 i i i个版本的线段树,表示的是 1 到 i 1到i 1i以来所有改变的值;

因此第 R R R个版本减去第 L − 1 L-1 L1个版本后;

我们就知道版本 [ L , R ] [L,R] [L,R]改变了什么;

也就是前缀和的思想


又因为我们是每遍历到一个元素就更新一个版本;

因此求区间 [ L , R ] [L,R] [L,R]就转化为了求版本 [ L , R ] [L,R] [L,R]


如下图所示,比如我们要区间 [ 2 , 4 ] [2,4] [2,4]第二小的数;

第一棵树是版本 4 4 4,第二棵树是版本 1 1 1

在这里插入图片描述
因为数值域 [ 1 , 2 ] [1,2] [1,2]只有一个数, [ 3 , 4 ] [3,4] [3,4]有两个数,因此我们去右子树找;

在右子树的左子树就找到了我们的答案;

Code

注意和普通线段树不同,我们的节点是不维护区间的;

节点中的 l , r l,r l,r是左右子树的坐标;

而我们存树则类似于图的链式前向星存法;

代码中的q代表新版本p代表旧版本!!!

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
#include <cmath>

using namespace std;

typedef long long ll;

const int N = 1e5 + 10;

struct Node{
    int lc,rc;//左右子树节点的编号
    int sum;//当前节点里面维护了多少个数
}tr[N*4 + N*17];

vector<int> ve;

//root(i)表示第i个版本的线段树根节点编号
int n,m,root[N],cnt,a[N];

//映射到[1,n]
int _get(int x){
    return 1 + lower_bound(ve.begin(),ve.end(),x) - ve.begin();
}
//在数值上建立线段树
int build(int l,int r){
    int p = ++cnt;
    if(l == r) return p;
    int mid = (l+r) >> 1;
    tr[p].lc = build(l,mid);
    tr[p].rc = build(mid+1,r);
    return p;
}
//更新版本
int update(int p,int l,int r,int val){
    int q = ++cnt;
    tr[q] = tr[p];//先复制上个版本
    if(l == r){
        ++tr[q].sum;
        return q;
    }
    int mid = (l+r) >> 1;
    if(val<=mid) tr[q].lc = update(tr[p].lc,l,mid,val);
    else tr[q].rc = update(tr[p].rc,mid+1,r,val);
    tr[q].sum = tr[tr[q].lc].sum + tr[tr[q].rc].sum;
    return q;
}
//返回第k小的数离散后的坐标
int query(int q,int p,int l,int r,int k){
    if(l == r) return l;
    int sum = tr[tr[q].lc].sum - tr[tr[p].lc].sum;
    int mid = (l+r) >> 1;
    //去左树
    if(k<=sum) return query(tr[q].lc,tr[p].lc,l,mid,k);
    //去右树
    else return query(tr[q].rc,tr[p].rc,mid+1,r,k-sum);
}
int main(){
    std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin >> n >> m;
    for(int i=1;i<=n;++i){
        cin >> a[i];
        ve.push_back(a[i]);
    }
    sort(ve.begin(),ve.end());
    ve.erase(unique(ve.begin(),ve.end()),ve.end());
    root[0] = build(1,ve.size());
    for(int i=1;i<=n;++i){
        root[i] = update(root[i-1],1,ve.size(),_get(a[i]));
    }
    int L,R,k,idx;
    while(m--){
        cin >> L >> R >> k;
        idx = query(root[R],root[L-1],1,ve.size(),k);
        cout << ve[idx-1] << '\n';
    }
    return 0;
}

例题二、

题面

洛谷P3919
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

思路

这题跟上题不太一样,有点传统线段树维护区间的味道;

上一道题更类似于权值线段树;

线段树节点维护的是左儿子、右儿子、当前节点的值;

因为这道题是类似维护区间,因此我们不需要离散化;

Code

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

typedef long long ll;

const int N = 1e6 + 10;

struct Node{
    int lc,rc,val;
}tr[N<<5];

int cnt,n,m,a[N],root[N];

int build(int l,int r){
    int q = ++cnt;
    if(l == r){
        tr[q].val = a[l];
        return q;
    }
    int mid = (l+r) >> 1;
    tr[q].lc = build(l,mid);
    tr[q].rc = build(mid+1,r);
    return q;
}
int update(int p,int l,int r,int loc,int val){
    int q = ++cnt;
    tr[q] = tr[p];
    if(l == r){
        tr[q].val = val;
        return q;
    }
    int mid = (l+r) >> 1;
    if(loc <= mid) tr[q].lc = update(tr[p].lc,l,mid,loc,val);
    else tr[q].rc = update(tr[p].rc,mid+1,r,loc,val);
    return q;
}
int query(int p,int l,int r,int loc){
    if(l == r){
        return tr[p].val;
    }
    int mid = (l+r) >> 1;
    if(loc <= mid) return query(tr[p].lc,l,mid,loc);
    else return query(tr[p].rc,mid+1,r,loc);
}
int main(){
    std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin >> n >> m;
    for(int i=1;i<=n;++i){
        cin >> a[i];
    }
    root[0] = build(1,n);
    for(int i=1,v,op,loc,val;i<=m;++i){
        cin >> v >> op;
        if(op == 1){
            cin >> loc >> val;
            root[i] = update(root[v],1,n,loc,val);
        }else{
            cin >> loc;
            root[i] = root[v];
            cout << query(root[v],1,n,loc) << '\n';
        }
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值