可持久化Trie and 线段树常见扩展
可持久化数据结构
可持久化Trie
思想概述
可持久化数据结构,是一种对原本数据结构进行的扩展,可以支持查询以前的历史版本的信息
在进行每一次操作的时候,我们都把需要更新的信息的节点全部创建一个新版本,起到节省空间的效果
关于这个算法的思想,就不多说了,书上有,给张照片就是
综合运用
众所周知,Tire树用于求异或最大值的操作,那么可持久化的Tire还可以支持求区间异或最大值操作,即给定一个值\(x\),求在指定区间\([l,r]\)中找一个值,使得这个值异或\(x\)最大
下面我们讨论如何完成这个操作
首先将\(a[i],i\in [1,n]\)全部按顺序插入Tire并创建历史版本,那么对于区间\([l,r]\)来说,对于保证右端点\(r\),就只需要在\(r\)的历史版本中查询即可,我们的问题就变为了如何保证左端点不小于\(l\),对于每一个节点维护一个值\(biggest\),表示该节点及其子树下面最大的一个数的编号,那么我们只需要在每一次进入分支的时候判断是否有\(biggest\ge l\),有就可以进去,否则不可以
这个玩意本身支持在尾部进行增加
若是要求连续的异或和,可以转为异或前缀和做
下面来个实际应用
Fotile 模拟赛L
\(FOTILE\) 得到了一个长为 \(N\) 的序列 \(A\),为了拯救地球,他希望知道某些区间内的最大的连续 \(XOR\) 和。
即对于一个询问,你需要求出 \(max(A_i\) \(xor\) \(A_{i+1}\) \(xor\) \(A_{i+2}\) … \(xor\) \(A_j)\),其中 \(l≤i≤j≤r\)。
为了体现在线操作,对于一个询问 \((x,y)\):
\(l=min(((x+lastans)\bmod N)+1,((y+lastans)\bmod N)+1)\)
\(r=max(((x+lastans)\bmod N)+1,((y+lastans)\bmod N)+1)\)
其中 \(lastans\) 是上次询问的答案,一开始为 0。
题意简述,求区间最大异或子段和,强制在线
做法:
首先我们会发现,由于求子段和的原因,我们肯定不能直接上一个可持久化Trie,应该是需要嵌套其他来达成目的
但,对于异或的维护,貌似没有什么数据结构可以优化,但,一瞅题目时空限制,此题就变成了一道小清新分块题。具体的,先将序列搞成前缀异或和,再将整个序列分为\(tot\)块,然后设\(F[i,j]\)表示块\(i-j\)的答案,那么我们最终的答案一定就是:在枚举\([l,L)\)枚举一个左端点,用可持久化Trie求异或最大值,记这个的最大值为\(A\),同理在\((R,r]\)枚举一个右端点,用可持久化Trie求异或最大值,记这个最大值为\(B\),我们就成功维护了可能涉及到\([l,L),(R,r]\)区间的可能答案,那么最终答案就是\(\max\lbrace A,B,F[pos(l)+1,pos(r)-1]\rbrace\),当然,若\(pos(r)-pos(l)\le 1\)就直接暴力了
需要注意的是,我们一次枚举左端点,一次枚举右端点,故我们需要两颗树分别维护这两个操作,一个是异或前缀和,一个是异或后缀和
那么预处理\(F\)该如何呢,我们可以使用递推的思想,首先枚举整块,直接\(O(n)\)暴力枚举左右端点,然后进行递推,对于\(F[i,j]->F[i,j+1]\),我们应该在\([L(j+1),R(j+1)]\)中枚举右端点找最大值(左端点为\(L(i)\)),与\(F[i,j]\)比较,若大于则更换
若我们取\(tot=\sqrt{n}\),则总复杂度为\(O(n\sqrt{n}\log V)\)
#include<bits/stdc++.h>
using namespace std;
#define debug(x) cerr << #x << ": " << (x) << endl
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define dwn(i,a,b) for(int i=(a);i>=(b);i--)
#define pb push_back
#define all(x) (x).begin(), (x).end()
#define x first
#define y second
using pii = pair<int, int>;
using ll = long long;
#define int long long
inline void read(int &x){
int s=0; x=1;
char ch=getchar();
while(ch<'0' || ch>'9') {if(ch=='-')x=-1;ch=getchar();}
while(ch>='0' && ch<='9') s=(s<<3)+(s<<1)+ch-'0',ch=getchar();
x*=s;
}
const int N=2e4+50, M=31, BLK=sqrt(N+1);
int n, Q;
int w[N];
struct LTrie{
int tr[N*M][2],root[N],s[N], mxid[N*M],idx;
int sum(int l, int r){
return s[r]^s[l-1];
}
void insert(int i, int k, int p, int q){
if(k==-1) return mxid[q]=i, void();
int val=s[i]>>k&1;
if(p) tr[q][val^1]=tr[p][val^1];
tr[q][val]=++idx;
insert(i, k-1, tr[p][val], tr[q][val]);
mxid[q]=max(mxid[tr[q][0]], mxid[tr[q][1]]);
}
void build(){
mxid[0]=-1;
root[0]=idx=1;
insert(0, M, 0, root[0]);
rep(i,1,n) s[i]^=s[i-1];
rep(i,1,n){
root[i]=++idx;
insert(i, M, root[i-1], root[i]);
}
}
int query(int u, int c, int lim){
dwn(i,M,0){
int val=c>>i&1;
if(mxid[tr[u][val^1]]>=lim) u=tr[u][val^1];
else u=tr[u][val];
}
return s[mxid[u]]^c;
}
int query(int l, int r){ // fix r and query limit >= l-1
return query(root[r], s[r], l-1);
}
}pre, suf;
int Len,tot,L[BLK],R[BLK],id[N]; // the block id of index
int f[BLK][BLK];
void init(){
tot=n/Len;
rep(i,1,tot) L[i]=(i-1)*Len+1, R[i]=i*Len;
if(R[tot]<n) ++tot, L[tot]=R[tot-1]+1, R[tot]=n;
rep(i,1,tot) rep(j,L[i],R[i]) id[j]=i;
// dp
rep(i,1,tot){
int l=L[i], r=R[i];
rep(j,l,r) rep(k,j,r) f[i][i]=max(f[i][i], pre.sum(j, k));
}
rep(len,2,tot){
rep(l,1,tot-len+1){ // l, r is block id
int r=l+len-1;
int lx=L[r], rx=R[r];
rep(i,lx,rx) f[l][r]=max(f[l][r], pre.query(L[l], i));
f[l][r]=max(f[l][r], f[l][r-1]);
}
}
}
int query(int l, int r){
int res=0;
int lid=id[l], rid=id[r];
if(lid==rid){
rep(i,l,r) res=max(res, pre.query(l, i));
return res;
}
if(lid+1<rid) res=f[lid+1][rid-1];
rep(i,l,L[lid+1]-1) res=max(res, suf.query(n-r+1, n-i+1));
rep(i,R[rid-1]+1,r) res=max(res, pre.query(l, i));
return res;
}
signed main(){
cin>>n>>Q;
rep(i,1,n) read(w[i]);
rep(i,1,n){
pre.s[i]=w[i];
suf.s[i]=w[n+1-i];
}
pre.build(), suf.build();
Len=sqrt(n+1);
init();
int res=0;
while(Q--){
int l, r; read(l), read(r);
l=(l+res)%n+1;
r=(r+res)%n+1;
if(l>r) swap(l, r);
cout<<(res=query(l, r))<<endl;
}
return 0;
}
可持久化线段树
可持久化线段树,又称函数式线段树,是最重要的可持久化数据结构之一
与可持久化Trie差不多的是,可持久化线段树也是在路径上创建新的版本,不过为了空间的需要,可持久化线段树一般就不初始建树了,直接动态开点
并且需要注意的是,可持久化线段树就不支持区间修改了,除非标记永久化
思想概述
大致思想与线段树基本一致
主席树(可持久化权值线段树)
可以使用主席树解决静态区间第K小问题,这里因蓝书上有思路,就不写了,给个代码即可
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+50;
int cnt[N<<5],root[N],lc[N<<5],rc[N<<5],tot,a[N],b[N],c[N],n,m;
#define mid (l+r>>1)
#define lson lc[rt],l,mid
#define rson rc[rt],mid+1,r
void build(int &rt,int l,int r){
rt=++tot;
if(l==r){cnt[rt]=0;return ;}
build(lson);build(rson);
}
int find(int p,int rt,int l,int r,int k){
if(l==r)return l;
int sum=cnt[lc[rt]]-cnt[lc[p]];
if(k<=sum)return find(lc[p],lson,k);
else return find(rc[p],rson,k-sum);
}
void insert(int p,int &rt,int l,int r,int x){
rt=++tot;
lc[rt]=lc[p],rc[rt]=rc[p],cnt[rt]=cnt[p]+1;
if(l==r)return ;
if(x<=mid)insert(lc[p],lson,x);
else insert(rc[p],rson,x);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
c[i]=a[i];
}
sort(a+1,a+n+1);
int sum=unique(a+1,a+n+1)-a-1;
for(int i=1;i<=n;i++){
b[i]=lower_bound(a+1,a+sum+1,c[i])-a;
}
build(root[0],1,sum);
for(int i=1;i<=n;i++)insert(root[i-1],root[i],1,sum,b[i]);
while(m--){
int l,r,k;
scanf("%d%d%d",&l,&r,&k);
printf("%d\n",a[find(root[l-1],root[r],1,sum,k)]);
}
}
可持久化数组
如题,你需要维护这样的一个长度为 N 的数组,支持如下几种操作
在某个历史版本上修改某一个位置上的值
访问某个历史版本上的某一位置的值
此外,每进行一次操作(对于操作2,即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从1开始编号,版本0表示初始状态数组)
根据题意,我们使用可持久化线段树来维护,对于修改操作,我们就像普通线段树的update
一样,但是边查询边创建新的节点,对于访问某历史版本的值就正常find即可,从root[x]找版本x上的值
#include<iostream>
#include<cstdio>
using namespace std;
int root[1000004], cnt;
struct node {
int sum, l, r, lc, rc;
}t[100000005];
void build(int& s, int l, int r) {
s = ++cnt;
t[s].l = l, t[s].r = r;
if (l == r){cin>>t[s].sum; return;}
int mid = l + r >> 1;
build(t[s].lc, l, mid);
build(t[s].rc, mid + 1, r);
t[s].sum = t[t[s].lc].sum + t[t[s].rc].sum;
}
void update(int &s, int pos, int dis, int k) {
s = ++cnt;
t[s] = t[pos];
if (t[s].l == dis && t[s].r == dis) {
t[s].sum = k;
return;
}
if (dis >= t[t[s].lc].l && dis <= t[t[s].lc].r) {
update(t[s].lc, t[pos].lc, dis, k);
}
else {
update(t[s].rc, t[pos].rc, dis, k);
}
t[s].sum = t[t[s].lc].sum + t[t[s].rc].sum;
}
int find(int pos, int dis) {
int s = pos;
if (t[s].l == dis && t[s].r == dis)return t[s].sum;
if (dis >= t[t[s].lc].l && dis <= t[t[s].lc].r) {
return find(t[s].lc, dis);
}
else {
return find(t[s].rc, dis);
}
}
int main() {
int n, m;
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
build(root[0], 1, n);
int tot = 0;
while (m--) {
int opt, x, y, z;
cin >> x >> opt;
if (opt == 1) {
cin >> y >> z;
update(root[++tot], root[x], y, z);
}
else {
root[++tot] = root[x];
cin >> y;
printf("%d\n", find(root[x], y));
}
}
return 0;
}
可持久化并查集
对于可持久化的并查集,我们需要支持的操作有
1 a b 合并 a,b 所在集合;
2 k 回到第 k 次操作(执行三种操作中的任意一种都记为一次操作)之后的状态;
3 a b 询问 a,b 是否属于同一集合,如果是则输出 1,否则输出 0。
结合可持久化数组的思想,我们只需要让可持久化数组支持单点修改和查询就可,但很明显不会这么简单
我们知道,并查集的常见优化有两种:路径压缩和按秩合并,这里我们采用按秩合并进行优化,路径压缩会炸空间
那么我们就需要维护按秩合并所需要的\(dep\)数组,于是我们就需要支持两个的可持久化
下面解释一下代码
建树:
#define Mid ((l+r)>>1)
#define lson L[rt],l,Mid
#define rson R[rt],Mid+1,r
// 整个代码的三个宏定义
void build(int &rt,int l,int r){
rt=++cnt;
if(l==r){fa[rt]=l;return ;}
build(lson);build(rson);
}
// 就是普通的可持久化数组构建法,不过维护的是Fa而已
合并
void merge(int p,int &rt,int l,int r,int x,int val){
rt=++tot;
L[rt]=L[p],R[rt]=R[p];
if(l==r){
fa[rt]=val;
dep[rt]=dep[p];
return ;
}
if(x<=mid)merge(L[p],lson,x,val);
else merge(R[p],rson,x,val);
}
修改节点深度
void update(int rt,int l,int r,int pos){
if(l==r){dep[rt]++;return ;}
if(pos<=Mid)update(lson,pos);
else update(rson,pos);
}
// 可持久化数组普通操作
查询某个值在可持久化数组里的下标
int query(int rt,int l,int r,int pos){
if(l==r)return rt;
if(pos<=Mid)return query(lson,pos);
else return query(rson,pos);
}
// 为了找祖先的操作
找祖先:
int find(int rt,int pos){
int now=query(rt,1,n,pos);
if(fa[now]==pos)return now;
return find(rt,fa[now]);
}
// 暴力找祖先
完整代码
#include<iostream>
#include<cstdio>
using namespace std;
#define N 300500
int root[N],L[N<<5],R[N<<5],fa[N<<5],dep[N<<5];
int n,m,tot;
namespace bcj{
#define mid ((l+r)>>1)
#define lson L[rt],l,mid
#define rson R[rt],mid+1,r
void build(int &rt,int l,int r){
rt=++tot;
if(l==r){fa[rt]=l;return ;}
build(lson),build(rson);
}
void merge(int p,int &rt,int l,int r,int x,int val){
rt=++tot;
L[rt]=L[p],R[rt]=R[p];
if(l==r){
fa[rt]=val;
dep[rt]=dep[p];
return ;
}
if(x<=mid)merge(L[p],lson,x,val);
else merge(R[p],rson,x,val);
}
void update(int rt,int l,int r,int x,int d){
if(l==r){dep[rt]+=d;return;}
if(x<=mid)update(lson,x,d);
else update(rson,x,d);
}
int query(int rt,int l,int r,int x){
if(l==r)return rt;
if(mid<x)return query(rson,x);
else return query(lson,x);
}
int find(int rt,int x){
int now=query(rt,1,n,x);
if(x==fa[now])return now;
else return find(rt,fa[now]);
}
#undef mid
#undef lson
#undef rson
}
using namespace bcj;
int main(){
scanf("%d%d",&n,&m);
build(root[0],1,n);
int ans=0;
for(int i=1;i<=m;i++){
int op,x,y;
scanf("%d",&op);
if(op==1){
scanf("%d%d",&x,&y);
// x^=ans;y^=ans;
root[i]=root[i-1];
int pos1=find(root[i],x);
int pos2=find(root[i],y);
if(fa[pos1]==fa[pos2])continue;
if(dep[pos1]>dep[pos2])swap(pos1,pos2);
merge(root[i-1],root[i],1,n,fa[pos1],fa[pos2]);
update(root[i],1,n,fa[pos2],1);
}
else if(op==2){
scanf("%d",&x);
// x^=ans;
root[i]=root[x];
}
else{
root[i]=root[i-1];
scanf("%d%d",&x,&y);
// x^=ans,y^=ans;
printf("%d\n",ans=fa[find(root[i],x)]==fa[find(root[i],y)]);
}
}
}