可持久化数据结构
可持久化数据结构
可持久化数据结构要求在每次进行数据结构的维护后都保存一个历史版本,并且支持对这些历史版本的数据结构进行再操作。
这就使得可持久化数据结构具有O(1)的版本维护和拷贝的特性。
比如O(1)复制整个数据结构,O(1)回退到某个历史版本。
我们想一下,其他的数据结构是否都能可持久化。
从方式来看,貌似所有的树形结构都可以扩展成可持久化的。
比如可持久化的堆,可持久化的并查集。
但是,真的是这样的么?
之前讲势能线段树的时候,我们讲,均摊势能的使用条件是,操作不可重现性。
可持久化因为O(1)版本回退的特性,使得所有操作都变为可重现操作,所以所有带均摊势能的数据结构都不支持可持久化
大体上绝大部分的平衡树都不支持可持久化,因为大部分树旋转都带有均摊势能。
除非这个树平衡不是靠旋转实现的,比如非旋treap。
可持久化字典树
对于一个01字典树,它有一个功能是支持以下两个操作
1、向当前集合中插入一个正整数
2、查询当前集合中亦或x最大的数字。
可持久化字典树的老本行,查询区间[L,R]的数字与x亦或的最大/最小值
1.区间异或和异或区间最大值异或区间最小值
虽然不是持久化字典树,但是字典树不错的题
如果只是求xorsum最大,我们可以维护一个前缀异或和的字典树,然后每次得到新的前缀异或和,先在字典树查一下能得到的最大值,再插入字典树。但是这里还有最大值和最小值,由于是统计区间信息,所以我们来考虑分治:
对于当前区间[l,r],统计出所有左端点在[l,mid],右端点在[mid+1,r]的区间的信息,然后再递归[l,mid]和[mid+1,r]。那么我们现在需要做的就是快速统计跨越mid的区间信息。这个时候可以分成四种情况:
1.最大值和最小值都在左边
2.最大值和最小值都在右边
3.最大值在左边,最小值在右边
4.最小值在左边,最大值在右边
对于第一种情况,我们在字典树上插入右半边可取的前缀异或和,然后对于每一个左端点查询 它的后缀异或和 ^ min ^ max可以在字典树上合出的最大值。
我们可以从大到小枚举左端点,可以发现左端点越小,右端点的可取范围就越大。所以每次改变左端点之后,就可以在字典树上插入信息。
第二张情况和第一种情况类似
对于第三种情况,最大值要在左边,最小值要在右边,所以右端点可以取的最小值和最大值都有限制,每次移动左端点的时候,要改变右端点范围,并根据范围在字典树上插入和删除值。
第四种情况和第三种类似。
这里有一个trick:可以只统计第一种情况和第三种情况,然后把数组倒过来再统计一遍,可以减少码量。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define pa pair<ll,int>
using namespace std;
const int maxn=4e6+101;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
const double eps=1e-12;
int read(){
int x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int n,a[maxn];
int tot,ch[maxn][2],cnt[maxn];
void clr(){
tot=0;ch[0][0]=ch[0][1]=0;cnt[0]=0;
return ;
}
void insert(int x){
int rt=0;
for(int i=30;i>=0;i--){
int t=(x>>i)&1;
if(!ch[rt][t]){
ch[rt][t]=++tot;
ch[tot][0]=ch[tot][1]=0;cnt[tot]=0;
}
rt=ch[rt][t];cnt[rt]++;
}
return ;
}
void del(int x){
int rt=0;
for(int i=30;i>=0;i--){
int t=(x>>i)&1;
rt=ch[rt][t];cnt[rt]--;
}
return ;
}
int query(int x){
int rt=0,res=0;
for(int i=30;i>=0;i--){
int t=(x>>i)&1;
if(cnt[ch[rt][!t]])res|=(1<<i),rt=ch[rt][!t];
else rt=ch[rt][t];
}
return res;
}
int ans,b[maxn],sum[maxn];
void solve(int l,int r){
if(l==r)return ;
int mid=(l+r)>>1;
solve(l,mid);solve(mid+1,r);
b[mid]=sum[mid]=0;
for(int i=mid+1;i<=r;i++){
b[i]=max(a[i],b[i-1]);
sum[i]=sum[i-1]^a[i];
}
int lp,rp;
int mx,mi,sm;
clr();
//最大小值在左边
mx=0;mi=inf;sm=0;rp=mid;
for(int i=mid;i>=l;i--){
mx=max(a[i],mx);mi=min(mi,a[i]);sm^=a[i];
while(rp<r && a[rp+1]<=mx && a[rp+1]>=mi)insert(sum[++rp]);
ans=max(ans,query(sm^mx^mi));
}
clr();
//最小值在左边,最大值在右边
mx=0;mi=inf;sm=0;lp=mid+1;rp=mid;
for(int i=mid;i>=l;i--){
mx=max(mx,a[i]);mi=min(mi,a[i]);sm^=a[i];
while(rp<r && a[rp+1]>=mi)insert(sum[rp+1]^b[rp+1]),rp++;
while(lp<=rp && b[lp]<mx)del(sum[lp]^b[lp]),lp++;
//lp指针把[lp,rp]内的不满足最大值大于左边的最大值的值在字典树删掉
ans=max(ans,query(sm^mi));
}
return ;
}
int main(){
n=read();
for(int i=1;i<=n;i++)a[i]=read(),ans=max(ans,a[i]);
solve(1,n);reverse(a+1,a+n+1);solve(1,n);
printf("%d",ans);
return 0;
}
2.牛牛的凑数游戏
首先考虑对一个区间如何找最小的无法表示的数
首先对该区间排序,第一个数必须是1,不然1无法被表示
第二个数要小于等于2(否则2无法被表示),若第二个数是2,那么第三个数要小于等于4(否则4无法被表示)
不难看出,前i个数的和为sum,那么第i+1个数要小于等于sum+1,否则sum+1无法被表示出来
如何优化区间小于等于sum+1的数字的和
不难想到可持久化数据结构
这道题可以发掘可持久化字典树的新用法,可以维护小于x的前缀和
首先将所有查询的sum值记录为1,pre(上一轮的sum)记录为0。
对于每个查询在l-1的位置记录小于等于sum+1的数字之和记为b,在r的位置记录小于等于sum+1的数字之和记为a。
然后更新该查询的答案为a-b,一旦某轮更新中发现当前轮的查询答案和上一轮相同。说明该查询已经得到了稳定的答案,不在参与下一轮迭代。
直到所有查询的答案都稳定下来,break结束算法并输出所有询问的答案。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define pa pair<ll,int>
#define u64 unsigned long long
using namespace std;
mt19937_64 mrand(random_device{}());
const int maxn=5e6+10101;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
const double eps=1e-12;
int read(){
int x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int n,m;
struct Trie{
int tot;
struct Tree{
int l,r;
ll sum;
}ch[maxn];
void insert(int &x,int y,ll v,int id){
ch[x=++tot]=ch[y];if(id<0){ch[x].sum+=v;return ;}
if(v&(1ll<<id))insert(ch[x].r,ch[y].r,v,id-1);
else insert(ch[x].l,ch[y].l,v,id-1);
ch[x].sum=ch[ch[x].l].sum+ch[ch[x].r].sum;
return ;
}
ll query(int x,ll v,int id){
v=min(v,(ll)inf);
if(id<0 || !x)return ch[x].sum;
ll ans=0;
if(v&(1ll<<id))ans=query(ch[x].r,v,id-1)+ch[ch[x].l].sum;
else ans=query(ch[x].l,v,id-1);
return ans;
}
}T;
int root[maxn];
int main(){
n=read();m=read();
for(int i=1;i<=n;i++)T.insert(root[i],root[i-1],read(),30);
for(int i=1;i<=m;i++){
int l=read(),r=read();
ll sum=1,last=0;
while(1){
ll cnt=T.query(root[r],sum,30)-T.query(root[l-1],sum,30);
if(cnt==last){printf("%lld\n",sum);break;}
else last=cnt,sum=cnt+1;
}
}
return 0;
}