可持久化数据结构
可持久化的前提条件
一个数据结构的拓补结构在使用的过程中保持不变。如线段树的结构在使用的过程中是不会变的,故其可持久化(就是主席树)。
而平衡树就不行,平衡树的旋转操作会改变节点之间的拓补序,所以带旋的平衡树都不可持久化。
可持久化解决的问题
可持久化就是通过每次修改都创建新版本,来保留数据结构回滚与访问历史版本的能力。
可持久化的核心思想
对于这个问题,其实我们有个暴力做法,即每次都创建一个新的数据结构备份。
这样做当然可以,但如果可持久化对象是如线段树一类开销本身就很大的数据结构,就会MLE甚至因为空间不足而RE。
并且每次创建一个完整的备份,时间上也不优秀。
那还有其他办法吗?
想想,当我们的游戏以及其他应用更新时,有多少是全部重新下载的?
几乎都是下载更新的部分,也就是增量更新。
说的具体点,只记录每一个版本上一版本不同的地方。这样的话我们的时空复杂度是有希望降低到一个较低的水平的。
又拿线段树举例子:我们知道线段树的空间要开到 (4n) 的级别,但每次修改只涉及到了 (4log n) 的节点,所以创建新版本期望时空复杂度能达到 (O(log n)) 。
可持久化的实例
Trie树的可持久化
先来一个最原始的Trie树:
依次维护cat
、rat
、cab
、fry
四个单词。
则原汁原味的Trie长成下面这个样子
(画渣用鼠标画的,别喷QAQ)
现在我们希望用可持久化的Trie来维护这几个单词。
第一个单词是cat
,直接插入进去:
第二个单词是rat
,首先我们要开一个新的根节点,它的节点信息有变化所以要裂开(?),它是我们新版本的入口。
我们将涉及到更新的所有节点都插入进去,注意这里的a
节点与路径cat
的a
节点在Trie中的意义不同,是两个互异的节点。
插入完成后,这棵树就长成了这样:
绿框中的就是版本2。
该更新版本3了,先复制上一个版本的信息......
但是c
点在要修改的路径上,所以c
要新建一个点。
所以复制裂开一个c
点,a
点同理。但是这个新的a
点又要指回原来的t
点。
为什么?
因为t
不在要修改的路径上啊。
完成插入后结构如图:
绿框中的即是版本3。
注意我们每次都只更新了一条路径,每次更新至于上一个版本比较,复制信息也从上一个版本复制。
按照上面的方法,更新fry
。
绿框中的即是版本4。
这就是可持久化Trie的过程了。
总之,只修改有关路径上的点。
可持久化线段树(主席树)
上面已经提到,线段树也是可持久化的。
每一次修改,都存下来当前的一个最新版本。
修改的时候与Trie也是一个道理,当某一次修改,一个节点的信息发生了改变,我们就 让这个点裂开 克隆一个全新的节点出来承载修改并继承原节点的信息。
每一次修改涉及 (O(4log n)) 个区间,设有 (m) 次操作,则额外的时空复杂度为 (O(mlog n)) 。
还需提及的是,这种线段树存储方式显然是不满足堆式存储的要求的,所以只能使用结构体式存储。
另外,可持久化线段树是很难以进行区间修改操作的。
具体原因出在懒标记下传需要更新的节点过多。当然,其实也可以用标记永久化来弄,但是标记永久化局限性也大。
言归正传,主席树的节点结构体是有一点变化的:
struct node
{
int lson,rson;//左右儿子的下标
int cnt;//当前区间有多少个数
}
不再存储区间了,直接存储了下标。
现在我们来看一下可持久化线段树如何单点修改
假设线段树的一次修改路径是这样的:
首先我们要做的还是把原来的节点复制出来,然后在复制出来的节点上做修改。
再把改变之前的边连上,得到下面这个东西:
想体现左右儿子的关系,画得很丑。
思路基本和trie一样,仍然是只修改路径上的点。
例题&代码实现
可持久化Trie:最大异或和
题目描述
给定一个非负整数序列 ({a_n}),初始长度为 (N)。
有 (M) 个操作,有以下两种操作类型:
-
A x
:添加操作,表示在序列末尾添加一个数 (x),序列的长度 (N) 增大 (1) 。 -
Q l r x
:询问操作,你需要找到一个位置 (p),满足 (l≤p≤r) ,使得:(a_p xor a_{p+1} xor … xor a_N xor x) 最大,输出这个最大值。
输入格式
第一行包括两个整数 (N,M),含义如题所述。
第二行包括 (N) 个非负整数,表示初始序列 ({a_n})。
接下来 (M) 行,每行描述一个操作,格式如题面所述。
输出格式
对于每一个询问,都输出一行一个整数表示答案。
输入样例
5 5
2 6 4 3 6
A 1
Q 3 5 4
A 4
Q 5 7 0
Q 3 6 6
输出样例
4
5
6
数据范围
(N,Mleq 3 imes 10^5,a_iin [0,10^7]∩Z)。
解析
由于异或有着类似减法的自反性,我们考虑前缀和。
设 (S_n=a_1 oplus a_2 oplus a_3 oplus dots oplus a_n),
则:(a_p oplus a_{p+1} oplus dots oplus a_n oplus x=S_n oplus S_{p-1} oplus x)。
题目的要求变为:在区间 ([L,R]) 内寻找一个最大的 (p) 使得 (S_{p-1} oplus C) 最大, (C=S_n oplus x)。
现在先忽视区间的限制,想想 ([1,N]) 怎么做。
考虑构建一棵trie树,树上每个节点为一个二进制位。也就是把各个数字拆成二进制处理。
这就是0-1trie。
然后根据异或的原理,每次尽量选取相反的值即可。
再考虑把区间右限制加上,就相当于查询这个trie的历史版本,所以我们需要可持久化。
可持久化的细节见代码。
处理左限制,我们可以再加一个信息max_id
:当前子树中下标的最大值。在寻找的时候我们判断一下max_id
是否大于 (L) ,是则继续递归,不是则返回。
code:
#include<bits/stdc++.h>
using namespace std;
const int N=6e5+10,M=N*25;
int n,m;
int s[N];
int tree[M][2]/*0-1trie,只有两种儿子*/,max_id[M];
int root[N],cnt=0;
/*极限空间大小:180M-190M*/
void insert(int i,int k,int p,int q)//i:当前前缀和下标,k:当前处理到了哪一个二进制位,p:上一版本,q:当前版本。
{
if(k<0)
{
max_id[q]=i;//叶节点保存下标信息
return ;
}
int nw= s[i]>>k&1;//当前位
if(p) tree[q][nw^1]=tree[p][nw^1];//复制同级另外一个节点的信息。
tree[q][nw]=++cnt;//插入当前点
insert(i,k-1,tree[p][nw],tree[q][nw]);
max_id[q]=max(max_id[tree[q][0]],max_id[tree[q][1]]);
}
int query(int root,int C,int L)//迭代查询
{
int p=root;
for(int i=23; i>=0; i--)
{
int nw=C>>i&1;// C的当前位
if(max_id[tree[p][nw^1]]>=L) p=tree[p][nw^1];//关于L的处理
else p=tree[p][nw];//贪
}
return C^s[max_id[p]];
}
int main()
{
scanf("%d%d",&n,&m);
max_id[0]=-1;//由于S0也是合法前缀和,根节点的max_id初始化成一个小于0的值
root[0]=++cnt;//创建初始版本
insert(0,23,0,root[0]);
for(int i=1; i<=n; i++)
{
int x;
scanf("%d",&x);
s[i]=s[i-1]^x;
root[i]=++cnt;
insert(i,23,root[i-1],root[i]);
}
char op[2];
int l,r,x;
for(int i=1; i<=m; i++)
{
scanf("%s ",op);
if(*op=='A')
{
scanf("%d",&x);
n++;
s[n]=s[n-1]^x;
root[n]=++cnt;
insert(n,23,root[n-1],root[n]);
}
if(*op=='Q')
{
scanf("%d%d%d",&l,&r,&x);
printf("%d
",query(root[r-1],s[n]^x,l-1));
}
}
}
可持久化线段树:区间第k小
>luoguP3834 【模板】可持久化线段树 2(主席树) <
题目描述
给定长度为 (N) 的整数序列 (A),下标为 (1∼N)。
现在要执行 (M) 次操作,其中第 (i) 次操作为给出三个整数 (l_i,r_i,k_i) ,求 (A[l_i] , A[l_i+1] … A[r_i]) (即 (A) 的下标区间 ([l_i,r_i]) )中第 (k_i) 小的数是多少。
输入格式
第一行包含两个整数 (N) 和M。
第二行包含 (N) 个整数,表示整数序列 (A) 。
接下来 (M) 行,每行包含三个整数(l_i,r_i,k_i),用以描述第 (i) 次操作。
输出格式
对于每次操作,都输出一行一个整数,表示该次操作中,第 (k_i) 小的数是多少。
输入样例
7 3
1 5 2 6 3 7 4
2 5 3
4 4 1
1 7 3
输出样例
5
6
3
数据范围
(Nleq 10^5, Mleq 10^4, |A[i]]|leq 10^9)
解析
这是一个静态序列的区间求k值的问题。
首先我们要明白,主席树本身在这个问题里面是不能支持动态序列的。
要动态区间,就要再套平衡树或者树状数组。这也不是本文的重点。
说回思路,本题求区间最值,首先我们能想到的是权值线段树。不知道的->线段树进阶——权值线段树与动态开点<
既然都离线了,先将所有的数都离散化一下。
然后在离散化后的值域上建立可持久化权值线段树。
一开始这棵树是空的,我们把序列的数一个一个插进去,那么每个版本的线段树就是只加前 (R) 个节点的权值线段树。
但是左范围不能像上一个题那样做,上个题由于是存在性问题,具有一定的特殊性。
不过,线段树有个很特殊的地方:它的结构几乎没什么变化,不像trie那样会多很多节点。可持久化的节点都能与之前版本中的节点一一对应起来的。所以,区间之间是能够做减法的。
也就是说我们可以通过减法,计算出 ([L,R]) 中位于值域区间 ([l,r]) 中的数的个数的。然后套入权值线段树的求k小中即可。
code:
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
struct node
{
int lson,rson;
int sum;
} tree[(N<<2)+N*17];
int n,m;
int arr[N];
vector<int> disc;
int root[N],tot=0;
int find(int x)
{
return lower_bound(disc.begin(),disc.end(),x)-disc.begin();
}
#define lnode tree[node].lson
#define rnode tree[node].rson
int build(int start,int end)//返回新建的节点编号
{
int node=++tot;
if(start==end) return node;
int mid=start+end>>1;
lnode=build(start,mid);
rnode=build(mid+1,end);
return node;
}
#define lnode1 tree[node1].lson
#define rnode1 tree[node1].rson
int update(int node,int start,int end,int val)//返回值同build
{
int node1=++tot;
tree[node1]=tree[node];//新建节点并复制信息
if(start==end)
{
tree[node1].sum++;
return node1;
}
int mid=start+end>>1;
if(val<=mid) lnode1=update(lnode,start,mid,val);
else rnode1=update(rnode,mid+1,end,val);
tree[node1].sum=tree[lnode1].sum+tree[rnode1].sum;
return node1;
}
int query(int node1,int node,int start,int end,int k)//查找区间k小值,涉及两个版本作差
{
if(start==end) return start;
int tmp=tree[lnode1].sum-tree[lnode].sum;
int mid=start+end>>1;
if(k<=tmp) return query(lnode1,lnode,start,mid,k);
else return query(rnode1,rnode,mid+1,end,k-tmp);//这里的处理参考权值线段树
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++)
{
scanf("%d",arr+i);
disc.push_back(arr[i]);
}
sort(disc.begin(),disc.end());
disc.erase(unique(disc.begin(),disc.end()),disc.end());//暴力离散化
int MAX=disc.size()-1;
root[0]=build(0,MAX);//建树,记录初始版本
for(int i=1; i<=n; i++)
{
root[i]=update(root[i-1],0,MAX,find(arr[i]));//一个一个插入
}
for(int i=1; i<=m; i++)
{
int l,r,k;
scanf("%d%d%d",&l,&r,&k);
printf("%d
",disc[query(root[r],root[l-1],0,MAX,k)]);//查询[l,r]第k小,l-1与r版本作差
}
return 0;
}