可持久化数据结构
1. 概念
完全可持久化:可以支持查询、修改历史版本数据的数据结构。
部分可持久化:可以支持查询历史版本,但修改仅限于当前版本的数据结构。
2. 记号
2.1. 复杂度分析:
无修改时: 采用 O ( A ) + O ( B ) + O ( C ) O(A)+O(B)+O(C) O(A)+O(B)+O(C)表示预处理复杂度+单次询问复杂度+空间复杂度。
有修改时: 采用 O ( A ) + O ( B ) + O ( C ) + O ( D ) O(A)+O(B)+O(C)+O(D) O(A)+O(B)+O(C)+O(D)表示预处理复杂度+单次询问复杂度+单次修改复杂度+空间复杂度。
3. 常见可持久化数据结构
3.1. 可持久化线段树
3.1.1. 无修改
举例子一个最经典的例子:无修改查询区间第 k k k大。
一个简单的想法就是建立 n n n棵权值线段树,第 i i i颗线段树维护的是区间 [ 1 , i ] [1,i] [1,i]每一个权值的数量,查询的时候直接对两棵线段树做差就可以得到该区间的情况。时间复杂度 O ( n 2 log n ) O(n^2\log n) O(n2logn)。
int Query(int x,int y,int k,int L=1,int R=n){//查询区间(x,y],权值区间[1,n]
if(L==R)return L;
int mid=(L+R)>>1,SumL=T[T[y].l].s-T[T[x].l].s;//(x,y]内小于等于中间权值的元素个数
if(k<=SumL)return Query(T[x].l,T[y].l,k,L,mid);
else return Query(T[x].r,T[y].r,k-SumL,mid+1,R);
}
可以注意到,第 i i i棵和第 i + 1 i+1 i+1课线段树只有 log n \log n logn个节点有差异(从根到叶子节点的一条路径)。那么我们可以不需要建立 n n n棵线段树,而是每棵线段树在继承前一棵线段树的基础上再新建 log n \log n logn个节点,于是可以得到如下插入代码:
sTuct Persistent_Tee{int l,r,s;}T[N*20];//s表示所维护的权值区间内有多少个元素
void Insert(int&p,int w,int L=1,int R=n){//离散后的权值在1~n之间
T[++Cnt]=T[p];p=Cnt;++T[p].s;//继承并新建一个节点,当前权值区间元素个数+1
if(L==R)return;int mid=(L+R)>>1;//剩下的与普通权值线段树一样
if(w<=mid)Insert(T[p].l,w,L,mid);
else Insert(T[p].r,w,mid+1,R);
}
复杂度: O ( n log n ) + O ( log n ) + O ( n log n ) O(n\log n)+O(\log n)+O(n\log n) O(nlogn)+O(logn)+O(nlogn)
题外话: 这种数据结构是由黄嘉泰发明的,由于他名字的拼音简写和某位领导人一样,所以我们将其称为主席树。[doge]
练习题:可持久化线段树 1、可持久化数组、Count on a Tee、[CQOI2015]任务查询系统…
3.1.2. 有修改
如果按照无修改的做法,每一次修改将会影响 O ( n ) O(n) O(n)棵线段树。但是考虑到其本质上是前缀和,所以可以参考单点修改,区间求和的做法,使用树状数组来维护。
具体做法为,第 i i i棵线段树改为维护区间 [ i − l o w b i t ( i ) , i ] [i-{\rm lowbit}(i),i] [i−lowbit(i),i]。这样每次修改最多会影响 log n \log n logn棵线段树,同样每次查询也需要获取 log n \log n logn棵线段树的线段树才能还原前缀和的信息。
复杂度: O ( n log 2 n ) + O ( log 2 n ) + O ( log 2 n ) + O ( n log 2 n ) O(n\log^2n)+O(\log^2n)+O(\log^2n)+O(n\log^2n) O(nlog2n)+O(log2n)+O(log2n)+O(nlog2n)
当然,可以保留按照无修改的做法得到的 n n n棵初始线段树,这样预处理的复杂度就是 O ( n log n ) O(n\log n) O(nlogn)了。
inline void modify(int i,int w,int val){//加上/减去第i个位置上权值为w的数
for(;i<=n;i+=i&-i)
Insert(T[i],1,m,w,val);//权值区间是[1,m]
}
inline void Get(int L,int R){//通过树状数组得到包含区间[1,L)和[1,R]信息的所有线段树
Tx=Ty=0;
for(int i=L-1;i;i-=i&-i)Stackx[++Tx]=T[i];
for(int i=R;i;i-=i&-i)Stacky[++Ty]=T[i];
}
inline void TansL(){//得到左子树信息
for(int i=1;i<=Tx;++i)Stackx[i]=T[Stackx[i]].l;
for(int i=1;i<=Ty;++i)Stacky[i]=T[Stacky[i]].l;
}
inline void TansR(){//得到右子树信息
for(int i=1;i<=Tx;++i)Stackx[i]=T[Stackx[i]].r;
for(int i=1;i<=Ty;++i)Stacky[i]=T[Stacky[i]].r;
}
int Qry(int L,int R,int k){//区间第k大
if(L==R)return L;
int mid=(L+R)>>1,SumL;
for(int i=1;i<=Ty;++i)SumL+=T[T[Stacky[i]].l].s;
for(int i=1;i<=Tx;++i)SumL-=T[T[Stackx[i]].l].s;//前缀和做差
if(k<=sp)return TansL(),Qry(L,mid,k);
else return TansR(),Qry(mid+1,R,k-SumL);
}
通常我们将其称为树套树。
练习题:二逼平衡树、[CQOI2011]动态逆序对…
3.1.3. 树上
树上路径第 k k k大: 线段树 T [ u ] T[u] T[u]维护的是根节点到 u u u路径上的信息。查询路径 u → v u\rightarrow v u→v信息只需将区间第 k k k的差值公式改为 T [ u ] + T [ v ] − T [ l c a ( u , v ) ] − T [ f a [ l c a ( u , v ) ] ] T[u]+T[v]-T[{\rm lca}(u,v)]-T[{\rm fa[lca}(u,v)]] T[u]+T[v]−T[lca(u,v)]−T[fa[lca(u,v)]]即可。
复杂度: O ( n log n ) + O ( log n ) + O ( n log n ) O(n\log n)+O(\log n)+O(n\log n) O(nlogn)+O(logn)+O(nlogn)
有修改: 注意到修改一个节点的权值只会影响其子树里的树的信息,也就是 d f s dfs dfs序中的一个区间,那么我们就可以用一个线段树来维护 d f s dfs dfs序中每个点的主席树,那么 T [ u ] T[u] T[u]就能表示成线段树中根到叶子路径上的点所代表的 O ( log n ) O(\log n) O(logn)棵权值线段树的和。
复杂度: O ( n log 2 n ) + O ( log 2 n ) + O ( log 2 n ) + O ( n log 2 n ) O(n\log^2n)+O(\log^2n)+O(\log^2n)+O(n\log^2n) O(nlog2n)+O(log2n)+O(log2n)+O(nlog2n)
扩展: 如果我们将最外层那个线段树持久化,就能回答历史版本的问题。
练习题:[CTSC2008]网络管理…
3.2. 可持久化 T r i e Trie Trie
与主席树的思路类似,可持久化 T r i e Trie Trie也是通过前缀和以及共用点来实现可持久化的。即对于每个字符串 S S S,我们都继承原来的树的基础上新建 ∣ S ∣ |S| ∣S∣个节点。
最常用也是最简单的可持久化 T r i e Trie Trie就是储存二进制数的二叉 T r i e Trie Trie树,由于异或运算为一个群,所以可以和加法一样通过维护前缀异或和来维护区间的异或信息。
最简单的例子:给定一个序列,每次询问 a l , . . . , a r a_l,...,a_r al,...,ar内和 w w w取异或运算的最大值是多少。
从高位开始查询该区间内是否存在该位与 x x x相反的数。
int Qry(int x,int y,int w){//查询区间(x,y]上与w异或的最大结果
int Ans=0;
for(int i=MaxD;i>=0;--i){
int c=(x>>i)&1;
if(Cnt[ch[y][!c]]>Cnt[ch[y][!c]])//贪心地查询与w在该位相反的数是否存在
Ans+=1<<i,x=ch[y][!c],y=ch[y][!c];
else x=ch[y][c],y=ch[y][c];
}
return Ans;
}
复杂度: O ( n log A ) + O ( log A ) + O ( n log A ) O(n\log A)+O(\log A)+O(n\log A) O(nlogA)+O(logA)+O(nlogA),其中 A = max i = 1 n log a i A=\max\limits_{i=1}^n \log a_i A=i=1maxnlogai
树上和带修改的可持久化 T r i e Trie Trie和主席树一样,就不在赘述了。
练习题:Tee、最大异或和、ALO、[THUSC2015]异或运算、L…
3.3. 可持久化并查集
可以注意到一点,并查集的可持久化实质上是 f a fa fa数组的可持久化,实质上就是可持久化数组。
值得注意的一点是,可持久化并查集不能进行路径压缩,只能使用按秩合并进行优化。因为单独是按秩合并已经可以将复杂度降为 O ( log 2 n ) O(\log^2n) O(log2n),而加上路径压缩后不但不能将时间复杂度优化成 α ( n ) log n \alpha(n)\log n α(n)logn,反而会将空间复杂度劣化到 O ( n log 2 n ) O(n\log^2n) O(nlog2n)。
模板题:可持久化并查集
先初始化。每棵树继承完成上一个操作后的树,当执行合并时,在原版本的树上新建节点并修改即可。
void build(int&p,int L,int R){//初始化fa[i]=i
if(!p)p=++Cnt;int mid=(L+R)>>1;
if(L==R)return(void)T[p].f=L;
build(T[p].l,L,mid),build(T[p].r,mid+1,R);
}
void modify(int&p,int L,int R,int a,int w){
T[++Cnt]=T[p],p=Cnt;//在原版本基础上新建节点
int mid=(L+R)>>1;
if(L==R)return(void)T[p].f=w;
if(a<=mid)modify(T[p].l,T[p].l,L,mid,a,w);
else modify(T[p].r,T[p].r,mid+1,R,a,w);
}
int qry(int p,int L,int R,int x){
if(L==R)return p;int mid=(L+R)>>1;
if(x<=mid)return qry(T[p].l,L,mid,x);
else return qry(T[p].r,mid+1,R,x);
}
void Add(int p,int L,int R,int x){//这一段可以写成非递归
if(L==R)return(void)++T[p].d;
int mid=(L+R)>>1;
if(x<=mid)Add(T[p].l,L,mid,x);
else Add(T[p].r,mid+1,R,x);
}
int GF(int I,int x){//查找父亲
int f=qry(I,1,n,x);//查询版本I中x的父亲
if(T[f].f==x)return f;
return GF(I,T[f].f);
}
inline void Link(int i,int x,int y){
int a=GF(T[i],x),b=GF(T[i],y);
if(T[a].f==T[b].f)return;
if(T[a].d>T[b].d)swap(a,b);//按秩合并
modify(T[i-1],T[i],1,n,T[a].f,T[b].f);
if(T[a].d==T[b].d)Add(T[i],1,n,T[b].f);//深度相同则合并后根深度+1
}
复杂度: O ( n log n ) + O ( log 2 n ) + O ( n log n ) O(n\log n)+O(\log^2n)+O(n\log n) O(nlogn)+O(log2n)+O(nlogn)
练习题:可持久化并查集、[NOI2018]归程…
3.4. 可持久化平衡树
3.4.1. 前言
值得一说的,势能分析类的数据结构,如 s p l a y splay splay和并查集,不能进行常规意义下的完全可持久化,即支持对历史版本的修改与询问。但是,它们通常可以利用可持久化数组的形式,完成部分可持久化,即仅支持对历史版本的询问,对于修改只针对于当前版本。这样,势能分析显然不会受到影响,因为势能分析数据结构不受询问影响。复杂度只需乘 log n \log n logn。
一个简单的反例为:不断地修改历史版本中深度为 n n n的那个节点。
所以可持久化平衡树就需要用到两种不基于旋转的平衡树: f h q T r e a p fhq\ Treap fhq Treap和替罪羊树。
这里只介绍相对简单的可持久化 f h q T r e a p fhq\ Treap fhq Treap。
3.4.2. 可持久化 f h q T r e a p \boldsymbol{fhq\ Treap} fhq Treap
由于 f h q T r e a p fhq\ Treap fhq Treap所有的操作都可以使用 m e r g e ( u , v ) merge(u,v) merge(u,v)和 s p l i t ( u , w ) split(u,w) split(u,w)来实现,故只需要讨论这两个操作如何可持久化即可。
一些记号: l e f t ( u ) , r i g h t ( u ) {\rm left}(u),{\rm right}(u) left(u),right(u)分别表示 u u u的左右孩子, s i z e ( u ) {\rm size}(u) size(u)表示 u u u子树的大小, k e y ( u ) {\rm key}(u) key(u)表示 u u u的随机权值, v a l ( u ) {\rm val}(u) val(u)表示 u u u的真实权值。
m e r g e ( u , v ) \boldsymbol{merge(u,v)} merge(u,v): 合并两个 T r e a p : u , v Treap:u,v Treap:u,v,其中 u u u中的所有元素都比 v v v小,最终返回一个包含了 u , v u,v u,v所有元素的 T r e a p Treap Treap。
实现: 首先如果 u , v u,v u,v中有一个为空,返回非空的那个即可。其次若 k e y ( u ) < k e y ( v ) {\rm key}(u)<{\rm key}(v) key(u)<key(v),则 u u u的左孩子不变,右孩子改为 m e r g e ( r i g h t ( u ) , v ) merge({\rm right}(u),v) merge(right(u),v);反之则 v v v的右孩子不变,左孩子改为 m e r g e ( u , l e f t ( v ) ) merge(u,{\rm left}(v)) merge(u,left(v))。注意到由于要求可持久化,所以修改是通过新建一个节点来实现的。
struct Node{int l,r,val,size,key;}T[N*20*2];//分别表示Treap上一个节点的左右儿子编号、真实权值、子树大小、随机权值
#define lc (T[u].l)
#define rc (T[u].r)
inline void up(int u){T[u].size=T[lc].size+T[rc].size+1;}//更新子树大小
int merge(int u,int v){
if(!u||!v)return u+v;//某一个为空则返回另一个
int p=++Cnt;//新建节点
return (T[u].key<t[v].hp?T[p]=t[u],T[p].r=merge(T[p].r,v)://根据情况来判断继承哪个节点
(T[p]=T[v],T[p].l=merge(u,t[p].l))),up(p),p;//更新合并后节点的子树大小并返回这个Treap
}
s p l i t ( u , w ) \boldsymbol{split(u,w)} split(u,w): 分裂 T r e a p u Treap\ u Treap u,返回两个 T r e a p : x , y Treap:x,y Treap:x,y,分别包含 u u u的真实权值小于等于 w w w的元素和其他元素。
实现: 如果 v a l ( u ) ≤ w {\rm val}(u)\le w val(u)≤w,令 { x , y } = s p l i t ( r i g h t ( u ) , w ) \{x,y\}={{\rm split}({\rm right}(u),w)} {x,y}=split(right(u),w),把 u u u的右子树改为 x x x,返回 { u , y } \{u,y\} {u,y}即可;反之则令 { x , y } = s p l i t ( l e f t ( u ) , w ) \{x,y\}={{\rm split}({\rm left}(u),w)} {x,y}=split(left(u),w),把 u u u的左子树改 y y y,返回 { x , u } \{x,u\} {x,u}即可。注意到由于要求可持久化,所以修改是通过新建一个节点来实现的。
void split(int u,int w,int&x,int&y){
if(!u)return(void)x=y=0;//空节点返回{0,0}
if(T[u].val<=w)T[x=+Cnt]=T[u],split(T[x].r,w,T[x].r,y),up(x);//更新时要继承并新建一个节点
else T[y=++Cnt]=T[u],split(T[y].l,w,x,T[y].l),up(y);//修改完后记得更新子树大小
}
其他操作 : 具体可以看这个。
复杂度: O ( n log n ) + O ( log n ) + O ( log n ) + O ( n log n ) O(n\log n)+O(\log n)+O(\log n)+O(n\log n) O(nlogn)+O(logn)+O(logn)+O(nlogn)
练习题:可持久化平衡树
4. 待填坑
关于可持久化的应用其实还有可持久化左偏树、可持久化后缀数组、可持久化块状链表、可持久化点分树、可持久化文艺平衡树、动态凸包等。具体题目有:
可持久化左偏树:[SDOI2010]魔法猪学院、POJ2449、HDU5960…
可持久化点分树:CF757G
可持久化文艺平衡树:可持久化文艺平衡树
可持久化块状链表: 尝试把主席树改写成块状链表、Count on a tree II、支持插入的区间第 k k k大、支持删除一段数和复制一段数插入到某个位置的区间第 k k k大…
可持久化后缀数组: 参考陈立杰的《可持久化后缀数据结构》
动态凸包:Almost、CF70D、[HAOI2011]防线修建