[数据结构]Treap

#0.0 写在前面

话说好久没在博客前写前言了呢

总之就是一直就想写几篇博客来专门记录一下平衡树的知识,可是总是咕咕咕QuQ,今天总算是把 fhq Treap 学了,趁热打铁赶紧来一发(。

Treap 分为两类

总体来讲都很好理解,fhq Treap 相对于传统的带旋 Treap 支持的操作好像更多(?)写起来也要简洁不少。

#1.0 带旋 Treap

我已经想不起来我上次写带旋 Treap 是啥时候了QwQ

咕咕咕~原因有贰:

  • 自己一年半之前写的带旋 Treap 实在是太丑了,不想再写一遍
  • 带旋 Treap 一般不会用到,fhq Treap 就够了。

#2.0 无旋 Treap (fhq Treap)

#2.1 基本架构

fhq Treap 如何解决平衡性的问题呢?引入一个附加权值,整体的结构对于所维护的权值满足 BST 的性质,对于附加权值满足堆序。那么用什么来作为附加权值呢?我们需要用他来使整棵树尽可能平衡,所以采用随机数作为附加权值。

struct Node {int val, tar, siz, ls, rs;} p[N];

inline void pushup(int k) {p[k].siz = p[p[k].ls].siz + p[p[k].rs].siz + 1;}
inline int new_node(int x) {p[++ cnt] = (Node){x, rand(), 1, 0, 0}; return cnt;}

fhq Treap 有很大的一个优点:短小精悍!他的主要函数只有两个:split()merge(),所有的其他操作又可以在运用分裂与合并得到很好的解决,并且这个东西可以搞除了 LCT 以外一切 bst,无旋 treap,splay 所能搞的东西。

#2.2 运转核心

#2.2.1 Split!

分裂是将一个 Treap 按权值(注意不是附加权值)变成两个 Treap 的过程,我们将权值小于等于 \(v\) 的部分分到一棵 Treap 中,剩下的分到另一棵 Treap 中。

分裂的过程也很好想,如果一个节点的权值小于等于 \(v\),那么左子树的节点的权值一定都小于等于 \(v\),去分裂右子树即可,反之亦然。

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

#2.2.2 Merge!

现在我们有两个 Treap,不妨叫他们左 Treap 和右 Treap,其中要求左 Treap 中的权值最大值要小于右 Treap 中的权值最小值。现在我们要将这两棵 Treap 合并。

如果当前左 Treap 节点的附加权值小于右 Treap 节点的附加权值,那么就将当前左 Treap 节点的右子树与当前右 Treap 节点继续进行合并;否则,就将当前左 Treap 节点与当前右 Treap 节点的左子树继续进行合并;

int merge(int x, int y) {
    if (!x || !y) return x + y;
    if (p[x].tar < p[y].tar) {
        p[x].rs = merge(p[x].rs, y);
        pushup(x); return x;
    } else {
        p[y].ls = merge(x, p[y].ls);
        pushup(y); return y;
    }
}

#2.3 各种操作

核心操作就这样结束了,剩下我们来看各种操作如何用 split()merge() 完成。

#2.3.1 插入

将 Treap 按插入值 \(v\) 进行分裂,再将三部分合并。

inline void insert(int v) {
    int x = 0, y = 0; split(rt, v, x, y);
    rt = merge(merge(x, new_node(v)), y);
}

#2.3.2 删除

将原 Treap 分裂为三部分:

  • 小于 v;
  • 等于 v;
  • 大于 v;

因为 fhq Treap 的节点不计重复,多个相同权值以不同的节点出现,所以我们要将第二部分的左右子树合并,这样根部的一个 \(v\) 就被删掉了,之后再将三部分合并即可。

inline void del(int v) {
    int x = 0, y = 0, z = 0;
    split(rt, v, x, z);
    split(x, v - 1, x, y);
    y = merge(p[y].ls, p[y].rs);
    rt = merge(merge(x, y), z);
}

#2.3.3 前驱

按 \(v-1\) 分裂,在权值小于 \(v\) 的树中尽可能向右走。

inline int pre(int v) {
    int x = 0, y = 0, pos; split(rt, v - 1, x, y);
    pos = x; while (p[pos].rs) pos = p[pos].rs;
    merge(x, y); return p[pos].val;
}

#2.3.4 后继

按 \(v\) 分裂,在权值大于 \(v\) 的树中尽可能向左走。

inline int nxt(int v) {
    int x = 0, y = 0, pos; split(rt, v, x, y);
    pos = y; while (p[pos].ls) pos = p[pos].ls;
    merge(x, y); return p[pos].val;
}

#2.3.5 求排名

按 \(v-1\) 分裂,权值小于 \(v\) 的树的大小加一即为排名。


void rk(int v) {
    int x = 0, y = 0; split(rt, v - 1, x, y);
    int ans = (p[x].siz + 1); rt = merge(x, y);
}

#2.3.6 排名第 k

非常普遍的操作方法,看左子树的大小是否大于等于 \(v\),

  • 是,进入左子树;
  • 否,大小刚好等于 \(v-1\),那么当前节点即为答案;
  • 否则进入右子树;
inline int kth(int k, int v) {
    while (true) {
        if (p[p[k].ls].siz >= v) k = p[k].ls;
        else if (p[p[k].ls].siz + 1 == v) return p[k].val;
        else v -= p[p[k].ls].siz + 1, k = p[k].rs;
    }
}

#3.0 经典练习

#3.1 P6136 【模板】普通平衡树(数据加强版)

就是上面的操作,注意强制在线和合并答案;

const int N = 1100010;
const int INF = 0x3fffffff;

struct Node {int val, tar, siz, ls, rs;} p[N];

int n, m, cnt, rt, lst, ans;

inline void pushup(int k) {p[k].siz = p[p[k].ls].siz + p[p[k].rs].siz + 1;}
inline int new_node(int x) {p[++ cnt] = (Node){x, rand(), 1, 0, 0}; return cnt;}

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

int merge(int x, int y) {
    if (!x || !y) return x + y;
    if (p[x].tar < p[y].tar) {
        p[x].rs = merge(p[x].rs, y);
        pushup(x); return x;
    } else {
        p[y].ls = merge(x, p[y].ls);
        pushup(y); return y;
    }
}

inline void insert(int v) {
    int x = 0, y = 0; split(rt, v, x, y);
    rt = merge(merge(x, new_node(v)), y);
}

inline void del(int v) {
    int x = 0, y = 0, z = 0;
    split(rt, v, x, z);
    split(x, v - 1, x, y);
    y = merge(p[y].ls, p[y].rs);
    rt = merge(merge(x, y), z);
}

void rk(int v) {
    int x = 0, y = 0; split(rt, v - 1, x, y);
    lst = (p[x].siz + 1); rt = merge(x, y);
}

inline int kth(int k, int v) {
    while (true) {
        if (p[p[k].ls].siz >= v) k = p[k].ls;
        else if (p[p[k].ls].siz + 1 == v) return p[k].val;
        else v -= p[p[k].ls].siz + 1, k = p[k].rs;
    }
}

inline int pre(int v) {
    int x = 0, y = 0, pos; split(rt, v - 1, x, y);
    pos = x; while (p[pos].rs) pos = p[pos].rs;
    merge(x, y); return p[pos].val;
}

inline int nxt(int v) {
    int x = 0, y = 0, pos; split(rt, v, x, y);
    pos = y; while (p[pos].ls) pos = p[pos].ls;
    merge(x, y); return p[pos].val;
}

int main() {
    srand((unsigned)time(NULL));
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++ i) {
        int x; scanf("%d", &x); insert(x);
    }
    while (m --) {
        int opt, x; scanf("%d%d", &opt, &x); x ^= lst;
        if (opt == 1) insert(x);
        else if (opt == 2) del(x);
        else if (opt == 3) rk(x);
        else if (opt == 4) lst = kth(rt, x);
        else if (opt == 5) lst = pre(x);
        else lst = nxt(x);
        if (opt >= 3) ans ^= lst;
    }
    printf("%d", ans);
    return 0;
}
上一篇:宋浩概率论与数理统计笔记——第八章


下一篇:微分方程数值解