可持久化数据结构即可以持久化的数据结构,维护所有历史版本。
可持久化线段树(主席树)
维护每个节点的左右儿子和权值。每一次修改时对于每个访问的点建立一个新节点,将这个节点修改后的权值和左右儿子填入新店内(本身权值不变)
CODE
void update(int pre,int &t,int l,int r,int pos){
t=++cnt;
ls[t]=ls[pre];
rs[t]=rs[pre];
sum[t]=sum[pre]+1;
if(l==r) return;
int mid=(l+r)/2;
if(pos<=mid) update(ls[pre],ls[t],l,mid,pos);
else update(rs[pre],rs[t],mid+1,r,pos);
}
int query(int pre,int t,int l,int r){
if(l==r) return b[l];
int mid=(l+r)/2;
int cmp=sum[ls[t]]-sum[ls[pre]];
if(cmp>=k) return query(ls[pre],ls[t],l,mid);
else{
k-=cmp;
return query(rs[pre],rs[t],mid+1,r);
}
}
可持久化数组与可持久化线段树等价
例题
CF323CTwo permutations
考虑对于
i
i
i时刻前
i
i
i行有哪些点,每次单点修改,用主席树维护,答案为
r
1
r_1
r1时刻的线段树
[
l
2
,
r
2
]
[l_2,r_2]
[l2,r2]的和减
l
1
l_1
l1-
1
1
1时刻的线段树
[
l
2
,
r
2
]
[l_2,r_2]
[l2,r2]的和。
可持久化并查集
用可持久化数组维护 f a fa fa数组,但由于某些不知名的原因,路径压缩空间会炸,所以只能按秩合并,所以还要额外用可持久化数组维护一个 d e p dep dep数组。
CODE
void bulid(int &p,int l,int r){
p=++tot;
if(l==r){t[p].sum=l;return;}
int mid=(l+r)/2;
bulid(t[p].l,l,mid);
bulid(t[p].r,mid+1,r);
}
void insert(int &p,int pre,int l,int r,int pos,int d){
t[++tot]=t[pre];p=tot;
if(l==r){t[p].sum=d;return;}
int mid=(l+r)/2;
if(pos<=mid) insert(t[p].l,t[pre].l,l,mid,pos,d);
else insert(t[p].r,t[pre].r,mid+1,r,pos,d);
}
int ask(int p,int l,int r,int pos){
if(l==r) return t[p].sum;
int mid=(l+r)/2;
if(pos<=mid) return ask(t[p].l,l,mid,pos);
else return ask(t[p].r,mid+1,r,pos);
}
int find(int v,int x){
int fx=ask(rtfa[v],1,n,x);
return fx==x?x:find(v,fx);
}
void merge(int v,int x,int y){
x=find(v-1,x);
y=find(v-1,y);
if(x==y){
rtfa[v]=rtfa[v-1];
rtdep[v]=rtdep[v-1];
}
else{
int depx=ask(rtdep[v-1],1,n,x);
int depy=ask(rtdep[v-1],1,n,y);
if(depx<depy){
insert(rtfa[v],rtfa[v-1],1,n,x,y);
rtdep[v]=rtdep[v-1];
}
else if(depx>depy){
insert(rtfa[v],rtfa[v-1],1,n,y,x);
rtdep[v]=rtdep[v-1];
}
else{
insert(rtfa[v],rtfa[v-1],1,n,x,y);
insert(rtdep[v],rtdep[v-1],1,n,y,depy+1);
}
}
}
例题
[NOI2018] 归程
1.可持久化并查集
先跑一边从
1
1
1号点到其他每个点的单源最短路
d
i
s
dis
dis。
对于每个询问,若保留海拔高于当前水位线的边,答案为当前点所以连通块内
d
i
s
dis
dis的最小值。
水位线降低时,将所有边按海拔从大到小依次加入,有可持久化并查集维护联通关系和每个连通块
d
i
s
dis
dis的最小值。
询问时二分当前访问的是那个版本的并查集。
2.Kruskal重构数
先跑一边从
1
1
1号点到其他每个点的单源最短路
d
i
s
dis
dis。
然后是要建出
k
r
u
s
k
a
l
kruskal
kruskal重构树,再然后
d
f
s
dfs
dfs维护以每个点作为根节点时子树中距离
1
1
1号点的最小花费。
最后限制点权>
p
p
p在树上倍增找到点
u
u
u。
可持久化平衡树
只有非旋
t
r
e
a
p
treap
treap支持可持久化。
对于每一个需要修改的点,新建一个节点即可。
CODE
int copynode(int x){
t[++cnt]=t[x];
return cnt;
}
int newnode(int x){
t[++cnt].v=x,t[cnt].sz=1,t[cnt].rnd=rand();
return cnt;
}
void update(int k){
if(k)
t[k].sz=t[t[k].ch[0]].sz+t[t[k].ch[1]].sz+1;
}
void split(int now,int k,int &x,int &y){
if(!now){x=0;y=0;return;}
if(t[now].v<=k){
x=copynode(now);
split(t[x].ch[1],k,t[x].ch[1],y);
}
else{
y=copynode(now);
split(t[y].ch[0],k,x,t[y].ch[0]);
}
update(x),update(y);
}
int merge(int x,int y){
if(!x || !y) return x+y;
if(t[x].rnd<t[y].rnd){
int z=copynode(x);
t[z].ch[1]=merge(t[z].ch[1],y);
update(z);
return z;
}
else{
int z=copynode(y);
t[z].ch[0]=merge(x,t[z].ch[0]);
update(z);
return z;
}
}
void insert(int now,int k){
int x=0,y=0,z=0;
split(rt[now],k,x,y);
z=newnode(k);
rt[now]=merge(merge(x,z),y);
}
void del(int now,int k){
int x=0,y=0,z=0;
split(rt[now],k,x,y);
split(x,k-1,x,z);
z=merge(t[z].ch[0],t[z].ch[1]);
rt[now]=merge(merge(x,z),y);
}
int rnk(int now,int k){
int x=0,y=0;
split(rt[now],k-1,x,y);
return t[x].sz+1;
}
int kth(int x,int k){
while(1){
if(t[t[x].ch[0]].sz+1 ==k) return t[x].v;
else if(t[t[x].ch[0]].sz>=k) x=t[x].ch[0];
else {k-=(t[t[x].ch[0]].sz+1);x=t[x].ch[1];}
}
}
int pre(int now,int k){
int x=0,y=0,z=0;
split(rt[now],k-1,x,y);
if(!x) return -INF;
return kth(x,t[x].sz);
}
int suf(int now,int k){
int x=0,y=0,z=0;
split(rt[now],k,x,y);
if(!y) return INF;
return kth(y,1);
}
可持久化trie树
原理也是相同的,而可持久化 01 t r i e 01trie 01trie又与可持久化线段树等价。
CODE
//以 01trie举例
int insert(int x,int val){
int t,y;t=y=++cnt;
for(int i=size;i>=0;i--){
ch[y][0]=ch[x][0],ch[y][1]=ch[x][1];
sum[y]=sum[x]+1;
int c=(val&(1<<i))>>i;
x=ch[x][c],ch[y][c]=++cnt,y=ch[y][c];
}
sum[y]=sum[x]+1;
return o;
}
int query(int l,int r,int val){
int ans=0;
for(int i=size;i>=0;i--){
int c=(val&(1<<i))>>i;
if(sum[ch[r][c^1]]-sum[ch[l][c^1]])
ans+=1<<i,r=ch[r][c^1],l=ch[l][c^1];
else r=ch[r][c],l=ch[l][c];
}
return ans;
}
例题
[十二省联考2019]异或粽子
求前缀异或和
p
r
e
pre
pre,则区间
[
l
,
r
]
[l,r]
[l,r]的美味度为
p
r
e
[
l
pre[l
pre[l-
1
]
1]
1]^
p
r
e
[
r
]
pre[r]
pre[r]。
按照区间右端点归并去权值前k大的区间,对于每个
r
r
r,维护以
r
r
r为右端点,没有选过的最大区间的权值,并放入一个堆中。每次选出当前最大的权值,再更新对应的右端点新的最大权值。
对应前缀建一个
t
r
i
e
trie
trie,在
t
r
i
e
trie
trie上进行一个类似于在线段树上二分的做法,用可持久化
t
r
i
e
trie
trie维护每个前缀的
t
r
i
e
trie
trie。
习题
[CTSC2018]混合果汁
将所有果汁按美味度从大到小排序,二分美味度。
考虑一个贪心:每次选择价格最小的果汁,直到体积满足要求。
所有每个点维护当前所表示的区间内果汁的总量和总价格,询问时线段树二分。
每个前缀都需要维护一个线段树,可使用主席树优化。
[APIO2020]交换城市
二分答案,保留小于等于当前答案的边。
当有一对点在一个连通块内且该连通块有环或至少包含一个度数大于2的点。
使用并查集维护,由于每个前缀都需要维护,所以要可持续化。
[国家集训队]middle
考虑二分答案。把小于当前答案的设为
−
1
-1
−1,大于等于它的设为
1
1
1。
[
a
,
b
]
[a,b]
[a,b]求一个最大后缀子段和,
[
c
,
d
]
[c,d]
[c,d]求一个最大前缀子段和
[
b
+
1
,
c
−
1
]
[b+1,c-1]
[b+1,c−1]求一个和。加起来如果大于等于
0
0
0,那么满足要求 且这个数还可以变大,否则就只能缩小。
每个数开一个线段树来做,所以,用主席树维护即可。
[HDUOJ]Boss Bo
对应每个
p
p
p,维护它与其它点的距离的最大
/
/
/最小
/
/
/和。
p
p
p变为其某一儿子
t
o
to
to时,
dis[s[to]][t[to]]-1,dis[1][s[to]-1]+1,dis[t[to]+1][n]+1
所以以to为根的线段树可由以为根的线段树修改得到,可用主席树维护。
[HDUOJ]GRE Words Once More!
考虑从一个点开始有多少不同的字符串:
f
[
i
]
f[i]
f[i]=
∑
f
[
j
]
\sum f[j]
∑f[j]+
s
[
i
]
s[i]
s[i]。
对于每个点,
w
s
o
n
[
i
]
wson[i]
wson[i] 表示
i
i
i 的所以出边中
f
f
f 值最大的,设所有
i
i
i 和
w
s
o
n
[
i
]
wson[i]
wson[i] 连边为重边,其余为轻边。
对于轻边直接二分,对于重边树上倍增即可。
[CF1340F]Nastya and CBS
对于一个区间,若它不能通过消除相邻的
x
x
x 和
−
x
-x
−x 使得其变成前面都是正数,后面都是负数的序列,我们称这个区间是不好的。
当一个区间是不好的,那么它和任意一个区间合并起来都是不好的。
所以可以不考虑不好的区间,用线段树维护合并区间,在每个节点用可持久化平衡树维护两个括号序列。