可持久化数据结构
可持久化平衡树
复习了一下fhq。
和主席树类似地,可持久化数据结构的精髓在于对每次进行次数为 \(log\) 级别的操作进行重开点,以此用尽可能小的时空损耗来保存每次操作完的全树状态。国内常用的可持久化平衡树是fhq,容易想到地,就是将它的split和merge操作进行可持久化。
inline int merge(int x,int y){
if(!x||!y)return x^y;
if(tree[x].key<tree[y].key){
int p=newnode();
tree[p]=tree[x];
rs(p)=merge(rs(p),y);
update(p);
return p;
}
else{
int p=newnode();
tree[p]=tree[y];
ls(p)=merge(x,ls(p));
update(p);
return p;
}
}
inline void split(int p,int k,int &x,int &y){
if(!p){
x=y=0;
return ;
}
if(tree[p].val<=k){
x=newnode();
tree[x]=tree[p];
split(rs(x),k,rs(x),y);
update(x);
}
else{
y=newnode();
tree[y]=tree[p];
split(ls(y),k,x,ls(y));
update(y);
}
}
再用 \(rt\) 数组存一下每个版本的起始根就好了。
带着区间(反转)tag的平衡树要求在所有“从上到下”类型的操作中进行下传,进而把下传操作放到merge和split里,同时进行可持久化。
inline int merge(int x,int y){
if(!x||!y)return x^y;
spread(x);
spread(y);
if(tree[x].key<tree[y].key){
rs(x)=merge(rs(x),y);
update(x);
return x;
}
else{
ls(y)=merge(x,ls(y));
update(y);
return y;
}
}
inline void split(int p,ll val,int &x,int &y){
if(!p){
x=y=0;
return ;
}
spread(p);
if(tree[ls(p)].siz<val){
x=copy(p);
split(rs(x),val-tree[ls(p)].siz-1,rs(x),y);
update(x);
}
else{
y=copy(p);
split(ls(y),val,x,ls(y));
update(y);
}
}
严厉谴责学校题单前一题还在板子后一题直接拉泡大的的罪恶行为。
发现后两个操作很有想象力,而且本题以足足64mb的空间限制被放入了一个可持久化数据结构题单中,太菜了于是果断被击败并查看题解。
先不管神秘的空间限制,对于操作二,相当于是取k个元素循环地填充满区间,可以考虑倍增地merge那k个元素所属的区间直到装不下,这个时候拆一段长度匹配的散块即可,这一过程有大量的重复节点,就可以用可持久化平衡树,对于操作三,可以在一开始建立rt0的可持久化树,每次操作3直接把当前根1的那段区间赋值成0的那一部分即可。
理想很美好,但是操作2中的复制操作会复制大量同样的fhq中的随机平衡因子key,这样我们直接就平衡了个寂寞。题解提出合并时用随机值判断:让子树大小大的成为父亲的概率高一些,能相对平衡一些。
inline void split(int p,int k,int &x,int &y){
if(!p){
x=y=0;
return ;
}
if(tree[ls(p)].siz+1<=k){
x=newnode();
tree[x]=tree[p];
split(rs(x),k-tree[ls(p)].siz-1,rs(x),y);
update(x);
}
else{
y=newnode();
tree[y]=tree[p];
split(ls(y),k,x,ls(y));
update(y);
}
}
inline int merge(int x,int y){
if(!x||!y)return x^y;
if(rnd()%(tree[x].siz+tree[y].siz)<tree[x].siz){
int p=newnode();
tree[p]=tree[x];
rs(p)=merge(rs(p),y);
update(p);
return p;
}
else{
int p=newnode();
tree[p]=tree[y];
ls(p)=merge(x,ls(p));
update(p);
return p;
}
}
大概就是上面这个样子。
但是空间问题还没解决!所以提出:每当节点个数超过一半的空间最大值就直接暴力重构,这样好像很大的时间限制就说的通了。放一下全代码。
#include<bits/stdc++.h>
#define MAXN 200005
#define MAXM 2000005
#define LIM 1000000
#define ll long long
using namespace std;
int n,m,mem;
const int inf=2e9;
int a[MAXN],top;
mt19937 rnd(time(0));
struct FHQ_Treap{
#define ls(p) tree[p].lson
#define rs(p) tree[p].rson
struct node{
int lson,rson;
ll sum;
int val,siz;
}tree[MAXM];
inline void update(int p){
tree[p].siz=tree[ls(p)].siz+tree[rs(p)].siz+1;
tree[p].sum=tree[ls(p)].sum+tree[rs(p)].sum+tree[p].val;
}
int tot,rt[2];
inline int newnode(ll val=0){
tree[++tot].val=val;
tree[tot].sum=val;
tree[tot].siz=1;
ls(tot)=rs(tot)=0;
return tot;
}
inline void split(int p,int k,int &x,int &y){
if(!p){
x=y=0;
return ;
}
if(tree[ls(p)].siz+1<=k){
x=newnode();
tree[x]=tree[p];
split(rs(x),k-tree[ls(p)].siz-1,rs(x),y);
update(x);
}
else{
y=newnode();
tree[y]=tree[p];
split(ls(y),k,x,ls(y));
update(y);
}
}
inline int merge(int x,int y){
if(!x||!y)return x^y;
if(rnd()%(tree[x].siz+tree[y].siz)<tree[x].siz){
int p=newnode();
tree[p]=tree[x];
rs(p)=merge(rs(p),y);
update(p);
return p;
}
else{
int p=newnode();
tree[p]=tree[y];
ls(p)=merge(x,ls(p));
update(p);
return p;
}
}
int x,y,z,w;
inline ll query(int &p,int l,int r){
x=y=z=0;
split(p,l-1,x,y);
split(y,r-l+1,y,z);
ll res=tree[y].sum;
p=merge(x,merge(y,z));
return res;
}
inline void modify(int &p,int l,int r,int k){
x=y=z=w=0;
split(p,r,x,z);
split(x,l-k-1,x,y);
split(y,k,y,p);
int tar=r-l+1+k;
while(tree[y].siz<=tar)y=merge(y,y);
int bin=0;
split(y,r-l+1+k,y,bin);
p=merge(x,merge(y,z));
}
inline void reset(int &p,int l,int r){
x=y=z=0;
split(rt[0],l-1,x,y);
split(y,r-l+1,y,z);
x=z=0;
int bin=0;
split(p,l-1,x,bin);
split(bin,r-l+1,bin,z);
p=merge(x,merge(y,z));
}
inline void build(int l,int r,int &p){
if(l>r){
p=0;
return ;
}
int mid=l+r>>1;
p=newnode(a[mid]);
build(l,mid-1,ls(p));
build(mid+1,r,rs(p));
update(p);
}
inline void dfs(int p){
if(!p)return ;
if(ls(p))dfs(ls(p));
a[++top]=tree[p].val;
if(rs(p))dfs(rs(p));
}
inline void reconstruct(){
top=0;
dfs(rt[1]);
tot=mem;
build(1,n,rt[1]);
}
}BT;
signed main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
BT.build(1,n,BT.rt[0]);
mem=BT.tot;
BT.rt[1]=BT.rt[0];
for(int i=1,opt,l,r,k;i<=m;i++){
scanf("%d%d%d",&opt,&l,&r);
if(opt==1)printf("%lld\n",BT.query(BT.rt[1],l,r));
else if(opt==2){
scanf("%d",&k);
BT.modify(BT.rt[1],l,r,k);
}
else BT.reset(BT.rt[1],l,r);
if(BT.tot>LIM)BT.reconstruct();
}
return 0;
}
好像这类题不是特别多,遇见了再记录吧。
可持久化Trie
这个还挺简单的,而且相当好用,主要的应用是解决区间而非全局异或问题。
这是一份可持久化01trie的板子,会主席树基本就可以自研了。
struct Trie{
#define ls(p) tree[p][0]
#define rs(p) tree[p][1]
int tree[MAXN*33][2],cnt[MAXN*33];
int tot,rt[MAXN];
inline void insert(int p,int pre,int val){//基于上个版本pre建立新版本p
for(int i=30;i>=0;i--){
cnt[p]=cnt[pre]+1;
if((val>>i)&1){
if(!rs(p))rs(p)=++tot;
ls(p)=ls(pre);
p=rs(p),pre=rs(pre);
}
else{
if(!ls(p))ls(p)=++tot;
rs(p)=rs(pre);
p=ls(p),pre=ls(pre);
}
}
cnt[p]=cnt[pre]+1;//记得跳到叶子也要加一下
}
inline int query(int x,int y,int val){//查区间lr内元素异或val的最大值
int res=0;
for(int i=30;i>=0;i--){
int k=(val>>i)&1;
if(cnt[tree[y][!k]]-cnt[tree[x][!k]]){
res+=(1<<i);
x=tree[x][!k];
y=tree[y][!k];
}
else x=tree[x][k],y=tree[y][k];
}
return res;
}
}Tr;
贪心地想:如果已经确定一个元素在这个区间是次大值,那对于这个元素肯定区间越大越好!可以单调栈跑出来每个元素左右侧第一个大于它的元素,并基于那个下标继续二分答案扩张(这样其实也就不用单调栈了),最后会生成两个它作为次大值的区间,于是对于每个 \(a_i\):
其中 \(f(l,r)\) 是区间 \(l,r\) 内异或 \(a_i\) 最大的值,单次可以拿01Trie在 \(O(logV)\) 复杂度求出,用可持久化Trie实现。
如果有动态添加的话,就可以直接考虑可持久化Trie了,然后观察一下询问是一个后缀和的性质,转化为求:
\(sum_i\) 表示前缀异或和。其中式子前两部分是定值,第三部分用可持久化Trie求解。
\(n,m\) 都很小啊,考虑与处理一下答案,给数列分块,设 \(sav_{l,r}\) 表示从第 \(l\) 个块到第 \(r\) 个元素间的最优解,这个是可以 \(O(n\sqrt n)\) 与处理的。对于每次询问,剩下的散块相当于询问长 \(O(\sqrt n)\) 的区间 \([l,r']\) 中的左端点和 \([l,r]\) 的右端点的最大子段异或和,忽视左右的影响,改成前缀异或和的形式然后拿可持久化Trie处理。
没啥说的,和树上主席树一个套路。
看起来很树剖啊。考虑用树剖的简化思想——dfs序解决问题,跑出dfs序然后对它建立可持久化Trie,则子树查询就是一个dfn区间,而路经查询就是若干个重链。
思路很简单但是写的时候不要犯迷糊了,转移到序列上就和树上父亲没关系了。
发现 \(n,q\) 都非常小,考虑用 \(n\) 个可持久化Trie来维护矩形,对于每次询问暴力查 \([x1,x2]\) 中的信息,把这段区间的Trie的cnt放到一起算,能走1就走走不了走0,贪心统计即可。
可持久化线段树
(以下内容基本照搬自我于2024.5.14发布的《近期进度小结》,有添加)
教你如何实现可持久化,这种先建全树后修改的结构不会出现在接下来的任何一道题中,就因为这个坑了lz半天没明白主席树是想干啥。
主席树可以理解为对区间信息的前缀和,如果信息可以通过一定方法相减的话。(这已经揭示主席树除了前缀和查区间信息还可树套树改区间元素)。
因为可持久化线段树每多一个版本只需要新建 \(O(log)\) 的节点,所以我们可以开 \(n\) 棵权值主席树维护 \([1,1],[1,2]...[1,n]\) 这样一坨前缀区间的权值树,权值树查kth很舒服。那么查询区间 \(kth\) 时,我们把第 \(r\) 版本的线段树和 \(l-1\) 版本的线段树的元素个数相减,在这个区间内进行跳 kth 的操作。当前元素差值个数 \(v\) 小于 \(k\) 时让两棵树一起跳跳右子树再查,否则就左子树。
注意到模式串长度固定,预处理每位引导的hash然后对它建权值主席树,每次在第 \(r->l-1\) 版本的树作差找权值就行了。
这个题可以莫队。
这个题有一种经典的处理方法,后面也会用。
维护一个 \(lst_{col}\) 数组表示颜色 \(col\) 上次出现的位置,按每一位建主席树的时候先copy上个版本,给这个版本这一位置的颜色+1,然后给这个版本于该位颜色的上次出现位置-1,这样第 \(i\) 棵主席树就解决了区间 \([1,i]\) 的同颜色答案重复问题。
查询 \([l,r]\) 时因为我们这次是下标而非权值主席树,在第 \(r\) 版本的主席树中查询 \([l,r]\) (其实 \([l,n]\) 也行因为这棵树后面没更新)的权值和即为答案。
如何在树上查询路径中的第 \(k\) 小权值?
考虑主席树的基本原理即前缀和(差分),既然序列上的静态问题可以用前缀和思想解决,那么树上的静态问题也是同理。
比如求路径和,那么可以预处理点到根节点的距离 \(dis_u\),可知两点的距离 \(dis(x,y)=dis_x+dis_y-dis_{lca}-dis_{fa_{lca}}\)。
同理地我们从根节点往下按dfn建权值主席树,那么点对 \((x,y)\) 的信息就这么作差:\(ans=rt_x+rt_y-rt_{lca}-rt_{fa_{lca}}\)
维护 \(x,y,lca,flca,l,r,k\) 六个信息跑权值主席树。
inline void modify(int l,int r,int x,int pre,int &p){
p=++tot;
tree[p]=tree[pre];
++tree[p].val;
int mid=l+r>>1;
if(l==r)return;
if(x<=mid)modify(l,mid,x,ls(pre),ls(p));
else modify(mid+1,r,x,rs(pre),rs(p));
}
inline int query(int l,int r,int lx,int rx,int lcax,int fx,int k){
int mid=l+r>>1;
if(l==r)return l;
int v=tree[ls(rx)].val+tree[ls(lx)].val-tree[ls(lcax)].val-tree[ls(fx)].val;
if(v>=k)return query(l,mid,ls(lx),ls(rx),ls(lcax),ls(fx),k);
else return query(mid+1,r,rs(lx),rs(rx),rs(lcax),rs(fx),k-v);
}
跳左右儿子的过程直观不好想,感性理解吧。
这里提一嘴启发式合并。
这个东西听起来就特别潮,一搜全是紫题,其实就是一个猪鼻优化。
别名 dsu on tree,树上并查集(雾,相似地,维护散点所属集团的根节点,比对合并。
最开始每个点的首领是他自己,每次找到两个点时,找到较小 (siz) 的那个集团然后直接暴力把小树插在大树上,对就是再对小树跑一遍 dfs 重新汇总答案。看起来是 \(O(n^2)\) 的,不过因为一些轻重链和势能问题,最后的复杂度是 \(O(nlog n)\) 的。证明网上有。
好现在看这道题。
如果没有 L 操作那么这道题就是上面的板题。现在考虑合并。既然建树的过程就是按树的结构造主席树,那每次合并就嗯和,连边,然后合并父亲,然后直接再建一次主席树。
inline void dfs(int u,int fa,int col){
vis[u]=col;
lcafa[0][u]=fa;
ST.modify(1,cnt,Val[u],ST.rt[fa],ST.rt[u]);
for(int i=1;i<=20;i++)lcafa[i][u]=lcafa[i-1][lcafa[i-1][u]];
dep[u]=dep[fa]+1;
siz[u]=1;
for(int i=h[u];i;i=edge[i].nxt){
int v=edge[i].v;
if(v==fa)continue;
dfs(v,u,col);
siz[u]+=siz[v];
}
}
...
if(opt[1]=='L'){
int fx=getf(x),fy=getf(y);
if(siz[fx]<siz[fy])swap(x,y),swap(fx,fy);
dfs(y,x,vis[x]);
siz[x]+=siz[y];
add(x,y);
add(y,x);
}
然后就好了。时间复杂度 \(O(nlog^2n)\)。
好多好玩的题都在bzoj上,谷没有水不了通过
这里复习一下我学成史的数论知识。
发现区间中这个 \(n\) 比较大,那就得从质因子入手了。
eulerphi 中质因子是不能重复算的,相当于要求这个区间中有去重后质因子之积。
这不就是颜色序列那道题嘛!维护每个质因子上次出现的位置,主席树中的权值改成积之值,然后就可以过了。
最开始感觉像是区间求mex那种东西,发现不太好维护信息。
但是可以从暴力开始优化。设现在已经可以表示 \([1,val]\) 内的数,那么答案 \(ans=val+1\)。
现在新加一个数 \(a_i\),要是 \(a_i>val+1\) 则 \(ans\) 不变。
要是 \(a_i\le val+1\) 则 \(ans=val+a_i+1\)。再假设区间 \([1,val]\) 是从所有值域在 \([1,x]\) 内的数拼凑出来的,那就有 \(a_i\in [x+1,val+1]\),每次可以找到 \([l,r]\) 内所有在这个区间的 \(a_i\)。则添加之后两个值域:\([1,val]->[1,val+\sum a_i],[1,x]->[1,val]\)。
找不到合适的 \(a_i\) 时相当于没法取了,把答案输出,\(a_i\) 的维护是主席树板子。
显然好的点对可以用单调栈求。之后用链表维护一下每个左端点对应的右端点然后按左端点建主席树。查询就从 \([l,r]\) 根区间查 \([l,r]\) 即可。
可以按深度为轴建立主席树,一个节点的颜色会对其父亲链作出1的贡献,维护颜色的lst并在每次更新时作差分即可实现去重。事先已经按照深度排序了之后再按dfn排会乱,所以直接拿set查前后继取深度较小的当成父亲节点即可。
ex:可持久化并查集
其实是主席树的一种扩展应用,没有找到足够的这方面的题目所以只放板子。
其实就是把并查集里的fa和siz给用主席树代替了。但是有一个问题:并查集的路径压缩 \(O(n\alpha (n))\) 时间复杂度是均摊的,而可持久化的实现必须依赖多项式复杂度的结构,所以要用到按秩合并。
按秩合并就是说,每次把深度小的合并到深度大的上(这样的话只要两者深度不等那大的深度一定不变,想想为什么),但是好像按大小合并也行,但是会被卡,参考上一篇文章(?。
然后按开头说的把数组变成可持久化数组就可以了...

浙公网安备 33010602011771号