引言
一、
可持久化数据结构的前提是其拓扑排序是不变的;
比如线段树,我们改变的总是它维护的数值,他的结构一旦定下来就不会变了;
二、
可持久化的核心思想是类似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 4∗n+nlog2n就够了;
如果要保险的话,开 n < < 5 n<<5 n<<5肯定够了…
例题一
思路
首先,因为数值比较大,范围比较小,因此我们离散化处理一下;
其次,我们在数值上建立一颗线段树(注意不是区间);
然后遍历数组的每一个元素,每遍历一个元素,我们就创建一个新的版本;
这样我们一共有 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 L−1个版本的线段树;
因为第 i i i个版本的线段树,表示的是 1 到 i 1到i 1到i以来所有改变的值;
因此第 R R R个版本减去第 L − 1 L-1 L−1个版本后;
我们就知道版本 [ 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;
}
例题二、
题面
思路
这题跟上题不太一样,有点传统线段树维护区间的味道;
上一道题更类似于权值线段树;
线段树节点维护的是左儿子、右儿子、当前节点的值;
因为这道题是类似维护区间,因此我们不需要离散化;
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;
}