进阶数据结构
update1 \(\text{(2026/07/07)}\) :为可持久化部分添加了习题。
平衡树
前言
\(\text{BST}\):二叉搜索树
\(\text{rotate}\):单次旋转操作
\(\text{splay}\):将节点 x 旋转至根
二叉搜索树:简单来说就是对于所有节点满足左子树全部小于自己,右子树全部大于自己。
鉴于此,二叉搜索树有着美妙的性质,比如中序遍历会得到一个不降序列,通过记录子树大小等信息可以快速查询排名等。
当我们要加入元素的时候,直接与根作比较,如果比根小则往左子树递归,否则往右子树递归。
删除元素时,先找到它的位置,然后直接删掉这个节点,就会多出 0-2 个子树。如果只有 1 个儿子,那么把这个儿子的子树接到原来的位置即可。
对于 2 个儿子的情况,让左儿子接替它的位置,那右儿子怎么办呢?根据二叉搜索树的性质,我们找到左儿子子树中最大的一个儿子,这个儿子肯定是右儿子,所以我们把右子树接到这个儿子的右边,不难发现此时形成的新树仍然满足二叉搜索树的性质。
可以参考下面的流程图理解:




然而毒瘤的出题人肯定没这么好心,只要我们从小到大插入数,树高就会被卡到 \(n\),此时操作的复杂度来到了 \(O(n)\),我们需要维护平衡树使其树高保持在 \(log n\) 左右。
Splay
\(\text{Splay}\) 的核心是通过对一些节点进行旋转,改变这棵树的结构,让它趋于平衡。

- 这张图上的数字为编号不是权值...
rotate: 本质上是将一个节点上移。比如上图右旋的过程相当于将节点 2 上移。
void rotate(int x){//旋转操作
int y = fa[x],z = fa[y],chk = get(x);//爹,爹的爹,是爹的哪个儿子
ch[y][chk] = ch[x][chk^1];//如果我是爹的左儿子,把我的右儿子给爹的左儿子//如果是右儿子,把我的左儿子给爹的右儿子
if(ch[x][chk^1])fa[ch[x][chk^1]] = y;//把这个儿子的爹改成我的爹
ch[x][chk^1] = y;//父子关系换了,哈哈!
fa[y] = x;fa[x] = z;//哈哈!哈哈!
if(z)ch[z][y == ch[z][1]] = x;//如果爹的爹存在,更新儿子,你滴儿子是我辣!
maintain(x);maintain(y);//pushup pushup
}
splay: 将一个节点通过不断的旋转转到根的位置。
void splay(int x){
for(int f;f = fa[x];rotate(x))//我还有爹吗,有就旋
if(fa[f])rotate(get(x) == get(f) ? f : x);//如果有爹,相同的话要先旋爹
root = x;//我是根辣!
}
一些辅助函数
inline int nw(int key){
return tr[++idx].key=key,tr[idx].siz=1,idx;
}
inline void push_up(int x){
tr[x].siz=tr[ls(x)].siz+tr[rs(x)].siz+1;
}
inline void clear(int x){
ls(x)=rs(x)=fa(x)=tr[x].siz=tr[x].key=0;
}
inline int get(int x){
return x==rs(fa(x));
}
插入
void ins(int key){
int now=rt,f=0;
while(now)f=now,now=tr[now].s[key>tr[now].key];
now=nw(key),fa(now)=f,tr[f].s[key>tr[f].key]=now,splay(now);
return ;
}
删除
void del(int key){
int now=rt,p=0;
while(now&&tr[now].key!=key)
p=now,now=tr[now].s[tr[now].key<key];
if(!now)return splay(p),void();
splay(now);
int cur=ls(now);
if(!cur)
return rt=rs(now),fa(rs(now))=0,clear(now),void();
while(rs(cur))cur=rs(cur);
rs(cur)=rs(now),fa(rs(now))=cur,fa(ls(now))=0,clear(now);
push_up(cur),splay(cur);
return ;
}
查询数x的前驱
int pre(int key){
int now=rt,ans=0,f=0;
while(now){
f=now;
if(tr[now].key>=key)now=ls(now);
else ans=tr[now].key,now=rs(now);
}
return splay(f),ans;
}
查询数x的后继
int nxt(int key){
int now=rt,ans=0,f=0;
while(now){
f=now;
if(tr[now].key<=key)now=rs(now);
else ans=tr[now].key,now=ls(now);
}
return splay(f),ans;
}
数x是第几大的(比x小的数的个数+1)
int rnk(int key){
int now=rt,ans=1,f=0;
while(now){
f=now;
if(tr[now].key<key)ans+=tr[ls(now)].siz+1,now=rs(now);
else now=ls(now);
}
return splay(f),ans;
}
查询第x大的数
int kth(int rk){
int now=rt;
while(now){
int sz=tr[ls(now)].siz+1;
if(sz>rk)now=ls(now);
else if(sz==rk)break;
else rk-=sz,now=rs(now);
}
return splay(now),tr[now].key;
}
FHQ Treap
没什么好说的,https://www.cnblogs.com/alex-wei/p/FHQTreap_Splay_LCT.html
可持久化数据结构
主席树zzz
简单来说,利用前缀和的思想以及动态开点的优势,主要处理区间第 \(k\) 小问题。
一般的,对于每个 \(i\),维护区间 \([1,i]\) 的权值线段树。考虑从 \([1,i-1]\) 到 \([1,i]\) 只进行了一次单点修改,只会改变 \(log(n)\) 个节点,记录下来,对每个 \(i\) 就有了不同的根 rt[i],称每一个 rt[i] 所对应的线段树为一个版本。(顺一张图片,来源)

然后是模板:
struct node{
int v,ls,rs;
}tr[N];//动态开点,存左右儿子
int rt[N],tot;//每个版本的根、节点个数
#define ls(A) tr[(A)].ls
#define rs(A) tr[(A)].rs
int nw(node x){
return tr[++tot]=x,tot;
}//新建节点
void build(int &p,int l,int r){
p=++tot;
if(l==r)return cin>>tr[p].v,void();
int mid=(l+r)>>1;
build(ls(p),l,mid),build(rs(p),mid+1,r);
return ;
}
void insert(int v,int &p,int l,int r,int pos,int x){
p=nw(tr[v]);//原来的版本为v,新建的版本为p
if(l==r)return tr[p].v=x,void();
int mid=(l+r)>>1;
if(pos<=mid)insert(ls(p),ls(p),l,mid,pos,x);
else insert(rs(p),rs(p),mid+1,r,pos,x);
return ;
}
int query(int p,int l,int r,int pos){
if(l==r)return tr[p].v;
int mid=(l+r)>>1;
if(pos<=mid)return query(ls(p),l,mid,pos);
else return query(rs(p),mid+1,r,pos);
}
习题
P2633 Count on a tree
一道主席树解决树上问题的板子,难度较低。考虑 \(u,v\) 之间的信息可以用 \(tr[u]+tr[v]-tr[lca(u,v)]-tr[fa[lca(u,v)]]\) 表示,这个是显然的,然后你发现离散化一下就做完了。
P5283 [十二省联考 2019] 异或粽子
先做一遍前缀异或和,考虑一个 \(n*n\) 的矩阵,\(A(i,j)=s_i\oplus s_j\),注意到这个矩阵会关于对角线对称,并且对角线上都是 \(0\)。对于每一个 \(a_i\) 求出 \(rk_{1,i}\),即所有 \(a_i \oplus a_j,1\le j\le n\) 中异或值第 \(1\) 大的 \(j\),然后丢到堆里,每次取最大的一个,不妨设取出来的为 \((x,y)\),表示其为与 \(a_i\) 的异或值中的 \(y\) 大的。统计答案,然后将 \((x,y+1)\) 丢到堆中,这样做的正确性自证不难。所以这和可持久化有什么关系
可持久化 Trie 练习题.

浙公网安备 33010602011771号