进阶数据结构

update1 \(\text{(2026/07/07)}\) :为可持久化部分添加了习题。

平衡树

前言
\(\text{BST}\):二叉搜索树
\(\text{rotate}\):单次旋转操作
\(\text{splay}\):将节点 x 旋转至根

二叉搜索树:简单来说就是对于所有节点满足左子树全部小于自己,右子树全部大于自己。
鉴于此,二叉搜索树有着美妙的性质,比如中序遍历会得到一个不降序列,通过记录子树大小等信息可以快速查询排名等。
当我们要加入元素的时候,直接与根作比较,如果比根小则往左子树递归,否则往右子树递归。
删除元素时,先找到它的位置,然后直接删掉这个节点,就会多出 0-2 个子树。如果只有 1 个儿子,那么把这个儿子的子树接到原来的位置即可。
对于 2 个儿子的情况,让左儿子接替它的位置,那右儿子怎么办呢?根据二叉搜索树的性质,我们找到左儿子子树中最大的一个儿子,这个儿子肯定是右儿子,所以我们把右子树接到这个儿子的右边,不难发现此时形成的新树仍然满足二叉搜索树的性质。

可以参考下面的流程图理解:

img
img
img
img

然而毒瘤的出题人肯定没这么好心,只要我们从小到大插入数,树高就会被卡到 \(n\),此时操作的复杂度来到了 \(O(n)\),我们需要维护平衡树使其树高保持在 \(log n\) 左右。

Splay

参考:https://www.cnblogs.com/ying-xue/p/17122409.html

\(\text{Splay}\) 的核心是通过对一些节点进行旋转,改变这棵树的结构,让它趋于平衡。

img
这张图上的数字为编号不是权值...

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] 所对应的线段树为一个版本。(顺一张图片,来源)
img

然后是模板:

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)\) 丢到堆中,这样做的正确性自证不难。所以这和可持久化有什么关系

P4735 最大异或和

可持久化 Trie 练习题.

势能线段树

posted @ 2026-07-06 20:28  GCSG01  阅读(5)  评论(0)    收藏  举报