Treaq(FHQ)

数据结构浅析

前置知识

BST(二叉搜索树) 性质:

  • 左子节点上的权值比父节点的权值小。
  • 右子节点上的权值比父节点的权值大。

既:\(left \lt root \lt right\)

堆的性质(大根堆):

  • 子节点的权值比父亲节点的权值小,既 \(root \geq left, \text{且}root \geq right\)(小根堆恰恰相反)

Treap的定义

Treap:树堆,“树堆”=“树”+“堆”,是一种既满足BST 性质又满足堆性质的一种数据结构。

Treap 的原理

容易发现,若使用同一个值作为两个数据结构的权值,那么两种数据结构结合后会变成一条链,所以平衡树在维护 BST 的权值 \(val\) 的基础上,引进了一个用堆维护的值 \(rd\)。其中 \(rd\) 的值随机给出。

这样维护,通过随机的 \(rd\) 值,打乱了节点插入的顺序,大大降低了因为插入顺序导致 BST 退化为链的可能,便能以 \(O(\log n)\)期望复杂度完成一系列操作。

无旋 Treap(FHQ Treap)

无旋 Treap 是一种简洁优美的数据结构,其代码量短较小,却几乎能支持 Treap 的所有功能。

FHQ Treap 有两种核心操作:分裂,合并。诸多复杂操作均基于这两种操作。

对于 FHQ Treap,节点要存的信息有其左儿子 \(ls\),右儿子 \(rs\),BST 权值 \(val\),堆的权值 \(rd\),子树大小 \(sz\)。FHQ Treap 将 \(val\) 相同的点看作多个节点,所以不用记录节点的存储的个数。

核心操作

新建节点

初始化一个有权值 \(val\) 的节点,毕竟平衡树维护的是一个个节点。

点击查看代码
int node; // 节点的编号

int newnode(int v) {
    x = ++node;
    tr[x].rd = rand(), tr[x].sz = 1, tr[x].val = v;
    return x;
}

更新子树大小

分裂或合并后总要更新的吧。
\(sz_{root} = sz_{ls} + sz_{rs}\)

点击查看代码
void push(int p) {tr[p].sz = tr[tr[p].ls].sz + tr[tr[p].rs].sz + 1; }

分裂

有两种分裂方式:按权值 \(val\) 分裂,另一种为按树的大小分裂。

按权值分裂

给定一个值 \(v\),要将原来的平衡树 \(T\) 分为两棵平衡树 \(T_x\)\(T_y\)\(\forall i \in T_x, j \in T_y, val_i \leq v \lt val_j\)

假设当前分裂到一个节点 \(p\),如果 \(val_p \leq v\),那么 \(p\) 应该属于 \(T_x\),且显然 \(p\) 的左子树均应该属于 \(T_x\),故只需向右递归寻找 \(p\) 的右子树有那些属于 \(T_x\),那些应该属于 \(T_y\) 即可。反之,\(p\) 应该属于 \(T_y\),故需要在 \(p\) 的左子树中寻找那些属于 \(T_x\),那些属于 \(T_y\)

如何拼接子树到平衡树上:让该子树的最大的父亲的父亲节点指向它即可。

点击查看代码
// P:当前分裂到的节点,v:分裂的值,x,y:将该节点子树分裂后的两棵平衡树
void split(int p, int v, int &x, int &y) {
    if (!p) { x = y = 0; return;} // 该节点不存在则返回
    if (tr[p].val <= v) split(tr[p].rs, v, tr[x = p].rs, y);
    else split(tr[p].ls, v, x, tr[y = p].ls);
    push(p);
}
按树的大小分裂

给定一个值 \(v\),要将原来的平衡树 \(T\) 分为两棵平衡树 \(T_x\)\(T_y\)\(sz_x=v,sz_y=n-v\)

与按权值分裂大致相同。

假设当前分裂到一个节点 \(p\),如果 \(sz_{p_{ls}} + 1 \leq v\),那么 \(p\) 应该属于 \(T_x\),且显然 \(p\) 的左子树均应该属于 \(T_x\),故只需向右递归寻找 \(p\) 的右子树有那些属于 \(T_x\),那些应该属于 \(T_y\) 即可。反之,\(p\) 应该属于 \(T_y\),故需要在 \(p\) 的左子树中寻找那些属于 \(T_x\),那些属于 \(T_y\)

点击查看代码
// P:当前分裂到的节点,v:分裂的值,x,y:将该节点子树分裂后的两棵平衡树
void split(int p, int v, int &x, int &y) {
    if (!p) { x = y = 0; return;} // 该节点不存在则返回
    if (tr[tr[p].ls] + 1 <= v) split(tr[p].rs, v, tr[x = p].rs, y);
    else split(tr[p].ls, v, x, tr[y = p].ls);
    push(p);
}

合并

能将两棵平衡树合并,如平衡树 \(T_x, T_y\),应该有 \(\forall i \in T_x, j \in T_y,i \le j\),这是由我们满足的,如果操作不满足这个性质,那么平衡树就不满足 BST 性质。

合并的过程:将一棵平衡树接到另外一棵平衡树上。

因操作满足 \(\forall i \in T_x, j \in T_y,i \le j\),所以仅需维护堆即可。
具体来说,若 \(x\)\(rd\) 大于 \(y\)\(rd\),则合并 \(x_{ls}\)\(y\),否则相反。

点击查看代码
int merge(int x, int y) {
    if (!x || !y) return x | y; // 如果合并过程中有一个树没有儿子,那么把有儿子的拼接上就行
    if (tr[x].rd > tr[y].rd) { // 维护堆性质
        tr[x].rs = merge(tr[x].rs, y), push(x);
        return x;
    }
    else {
        tr[y].ls = merge(x, tr[y].ls), push(y);
        return y;
    }
}

例题

luogu P3369【模板】普通平衡树

平衡树模板题,需要支持 6 种操作:

  1. 插入一个数
  2. 删除一个数
  3. 查询一个数的排名(排名为比当前数小的数的个数 \(+1\)
  4. 查询排名为 \(x\) 的数
  5. \(x\) 的前驱(小于 \(x\) 且最大的数)
  6. \(x\) 的后驱(大于 \(x\) 且最小的数)

插入

设插入一个数 \(x\),将平衡树 \(T\)\(x - 1\) 分裂成两棵平衡树 \(T_x\)\(T_y\)\(\forall i \in T_x, j \in T_y, i \lt x, x \lt j\)

新建一个 \(val\)\(c\) 的节点,先将 \(T_x\)\(c\) 节点合并得新的 \(T_x\),再将 \(T_x\)\(T_y\) 合并即可。

点击查看代码
void insert(int v) {
    int x = 0, y = 0;
    split(R, v - 1, x, y);// R 是根节点
    R = merge(merge(x, newnode(v)), y);
}

删除

设删除一个数 \(x\),将平衡树 \(T\)\(x - 1,x\) 分裂出三棵平衡树 \(T_x,T_y,T_z\)

然后合并 \(T_y\) 的左右儿子(没有左右儿子合并是 \(0\),没有影响)得到新的 \(T_y\)(这样可以认为把 \(T_y\) 的根去掉了)然后把三棵平衡树合并起来即可。

点击查看代码
void _delete(int v) {
    int x = 0, y = 0, z = 0;// R 是根节点
    split(R, v - 1, x, y), split(y, v, y, z);
    R = merge(merge(x, merge()))
}

查询排名

统计比 \(v\) 大的数有多少即可,将平衡树 \(T\) 分裂为两棵平衡树 \(T_x,T_y\),其中 \(\forall i \in T_x,j\in T_y,i \lt v \leq j\)。答案就是 \(sz_x\),注意分裂后要合并还原。

点击查看代码
int rank(int v) {
    int x = 0, y = 0,ans = 0;
    split(R, x - 1, x, y);
    ans = tr[v].sz;
    R = merge(x, y);
    return ans;
}

查询排名为 \(k\) 的值

因为平衡树满足 BST 性质,所以查询第 \(k\) 小的流程很简单。

具体来说:若当前查询到一点 \(p\),如果它的左儿子的 \(sz\)\(k\) 大,那么答案一定在左子树里。而若它的左子树的 \(sz\) 加上 \(1\) 等于 \(k\),说明 \(p\) 就是第 \(k\) 小。否则,答案一定在右子树里,向右递归即可。

点击查看代码
int kth(int k) {
    int p = R;

    while (1) {
        if (tr[tr[p].ls].sz <= k) p = tr[p].ls;
        else if (tr[tr[p]].ls + 1 == k) return tr[p].val;
        else k -= tr[p].sz + 1, p = tr[p].rs; 
    }
}

查询前驱

与查询第 \(k\) 大很像。
因为要找最大的比 \(x\) 小的值,所以查询过程中,若当前点比 \(x\) 小,更新答案,递归到右子树即可,否则,递归左子树。

点击查看代码
int pre(int v) {
    int p = R, ans = 0;

    while (1) {
        if (!p) return ans;
        else if (v <= tr[p].val) p = tr[p].ls;
        else ans = tr[p].val,p = tr[p].rs;
    }
}

查询后驱

与查询前驱反过来即可。

点击查看代码
int suc(int v) {
    int p = R, ans = 0;

    while (1) {
        if (!p) return ans;
        else if (v >= tr[p].val) p = tr[p].rs;
        else ans = tr[p].val, p = tr[p].ls;
    }
}

完整代码

点击查看代码
#include <iostream>
#include <cstdio>

using namespace std;

void RD() {}
template<typename T, typename... U> void RD(T &x, U&... arg) {
    x = 0; int f = 1;
    char ch = getchar();
    while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
    while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
    x *= f; RD(arg...);
}

const int N = 1e5 + 5;

struct tree { int val, rd, sz, ls, rs; } tr[N];
int n, node, R;

int newnode(int v) {
    int x = ++node;
    tr[x].val = v, tr[x].rd = rand(), tr[x].sz = 1;
    return x;
}

void push(int p) { tr[p].sz = tr[tr[p].ls].sz + tr[tr[p].rs].sz + 1; }

void split(int p, int v, int &x, int &y) {
    if (!p) { x = y = 0; return; }
    if (tr[p].val <= v) split(tr[p].rs, v, tr[x = p].rs, y);
    else split(tr[p].ls, v, x, tr[y = p].ls);
    push(p);
}

int merge(int x, int y) {
    if (!x || !y) return x | y;

    if (tr[x].rd > tr[y].rd) {
        tr[x].rs = merge(tr[x].rs, y);
        push(x); return x;
    }
    else {
        tr[y].ls = merge(x, tr[y].ls);
        push(y); return y;
    }
}

void insert(int v) {
    int x = 0, y = 0;
    split(R, v - 1, x, y);
    R = merge(merge(x, newnode(v)), y);
}

void _delete(int v) {
    int x = 0, y = 0, z = 0;
    split(R, v - 1, x, z), split(z, v, y, z);
    R = merge(merge(x, y = merge(tr[y].ls, tr[y].rs)), z);
}

int rand(int v) {
    int x = 0, y = 0, ans = 0;
    split(R, v - 1, x, y);
    ans = tr[x].sz + 1;
    R = merge(x, y);
    return ans;
}

int kth(int k) {
    int  p = R;

    while (1) {
        if (k <= tr[tr[p].ls].sz) p = tr[p].ls;
        else if (k == tr[tr[p].ls].sz + 1) return tr[p].val;
        else k -= tr[tr[p].ls].sz + 1, p = tr[p].rs;
    }
}

int pre(int v) {
    int p = R, ans = 0;

    while (1) {
        if (!p) return ans;
        else if (v <= tr[p].val) p = tr[p].ls;
        else ans = tr[p].val,p = tr[p].rs;
    }
}

int suc(int v) {
    int p = R, ans = 0;
    while (1) {
        if (!p) return ans;
        else if (v >= tr[p].val) p = tr[p].rs;
        else ans = tr[p].val, p = tr[p].ls;
    }
}

int main() {
    RD(n);
    while (n--) {
        int opt, x;
        RD(opt, x);
        if (opt == 1) insert(x);
        if (opt == 2) _delete(x);
        if (opt == 3) printf("%d\n", rand(x));
        if (opt == 4) printf("%d\n", kth(x));
        if (opt == 5) printf("%d\n", pre(x));
        if (opt == 6) printf("%d\n", suc(x));
    }
    return 0;
}

我们能够爱的人,我们也能恨他们。而其余的人,则对我们无关紧要。

posted @ 2024-08-08 11:12  FRZ_29  阅读(14)  评论(2编辑  收藏  举报