可持久化数据结构
可持久化平衡树
复习了一下fhq。
和主席树类似地,可持久化数据结构的精髓在于对每次进行次数为
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);
}
}
再用
带着区间(反转)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;
贪心地想:如果已经确定一个元素在这个区间是次大值,那对于这个元素肯定区间越大越好!可以单调栈跑出来每个元素左右侧第一个大于它的元素,并基于那个下标继续二分答案扩张(这样其实也就不用单调栈了),最后会生成两个它作为次大值的区间,于是对于每个
其中
如果有动态添加的话,就可以直接考虑可持久化Trie了,然后观察一下询问是一个后缀和的性质,转化为求:
没啥说的,和树上主席树一个套路。
看起来很树剖啊。考虑用树剖的简化思想——dfs序解决问题,跑出dfs序然后对它建立可持久化Trie,则子树查询就是一个dfn区间,而路经查询就是若干个重链。
思路很简单但是写的时候不要犯迷糊了,转移到序列上就和树上父亲没关系了。
发现
可持久化线段树
(以下内容基本照搬自我于2024.5.14发布的《近期进度小结》,有添加)
教你如何实现可持久化,这种先建全树后修改的结构不会出现在接下来的任何一道题中,就因为这个坑了lz半天没明白主席树是想干啥。
主席树可以理解为对区间信息的前缀和,如果信息可以通过一定方法相减的话。(这已经揭示主席树除了前缀和查区间信息还可树套树改区间元素)。
因为可持久化线段树每多一个版本只需要新建
注意到模式串长度固定,预处理每位引导的hash然后对它建权值主席树,每次在第
这个题可以莫队。
这个题有一种经典的处理方法,后面也会用。
维护一个
查询
如何在树上查询路径中的第
考虑主席树的基本原理即前缀和(差分),既然序列上的静态问题可以用前缀和思想解决,那么树上的静态问题也是同理。
比如求路径和,那么可以预处理点到根节点的距离
同理地我们从根节点往下按dfn建权值主席树,那么点对
维护
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 重新汇总答案。看起来是
好现在看这道题。
如果没有 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);
}
然后就好了。时间复杂度
好多好玩的题都在bzoj上,谷没有水不了通过
这里复习一下我学成史的数论知识。
发现区间中这个
eulerphi 中质因子是不能重复算的,相当于要求这个区间中有去重后质因子之积。
这不就是颜色序列那道题嘛!维护每个质因子上次出现的位置,主席树中的权值改成积之值,然后就可以过了。
最开始感觉像是区间求mex那种东西,发现不太好维护信息。
但是可以从暴力开始优化。设现在已经可以表示
现在新加一个数
要是
找不到合适的
显然好的点对可以用单调栈求。之后用链表维护一下每个左端点对应的右端点然后按左端点建主席树。查询就从
可以按深度为轴建立主席树,一个节点的颜色会对其父亲链作出1的贡献,维护颜色的lst并在每次更新时作差分即可实现去重。事先已经按照深度排序了之后再按dfn排会乱,所以直接拿set查前后继取深度较小的当成父亲节点即可。
ex:可持久化并查集
其实是主席树的一种扩展应用,没有找到足够的这方面的题目所以只放板子。
其实就是把并查集里的fa和siz给用主席树代替了。但是有一个问题:并查集的路径压缩
按秩合并就是说,每次把深度小的合并到深度大的上(这样的话只要两者深度不等那大的深度一定不变,想想为什么),但是好像按大小合并也行,但是会被卡,参考上一篇文章(?。
然后按开头说的把数组变成可持久化数组就可以了...
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了