可持久化数据结构

可持久化数据结构

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] [ilowbit(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 uv信息只需将区间第 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]魔法猪学院POJ2449HDU5960

可持久化点分树:CF757G

可持久化文艺平衡树:可持久化文艺平衡树

可持久化块状链表: 尝试把主席树改写成块状链表、Count on a tree II、支持插入的区间第 k k k大、支持删除一段数和复制一段数插入到某个位置的区间第 k k k大…

可持久化后缀数组: 参考陈立杰的《可持久化后缀数据结构》

动态凸包:AlmostCF70D[HAOI2011]防线修建

拓展题:[NOI2018]你的名字谢特[HEOI2016/TJOI2016]字符串middle

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值