笔记:FHQ Treap 之三分裂
小知识点,但是好像没什么人写,所以写一篇。 在 NOIP 之前积攒一点 rp。
需要的知识
- 平衡树(FHQ Treap)
前言
一般在写 FHQ Treap 的时候,都是按照某个值或排名 \(k\),将 Treap 分成小于等于和大于 \(k\) 的两棵树,我们将其称为二分裂。那么,所谓三分裂,就是将 Treap 按照小于、等于、大于 \(k\) 分裂成三棵树。
详解
在二分裂中,我们在分裂函数中传入两个引用 &L, &R,最后这棵树被分裂为分别以 L, R 为根的两棵树。现在我们传入三个引用 &L, &M, &R,当分裂函数执行完后,以 L, M, R 为根的三棵树分别包含了原先小于、等于、大于 \(k\) 的节点。
这样做有什么用呢?其实就是可以比较方便地得到等于 \(k\) 的节点的信息。其实二分裂也是可以做到的,叁分裂主要是种技巧,有时候用起来比较方便。 下面还是来看一道题。
例题
色球
题目描述
牛牛有 \(n\) 种颜色的彩色小球(编号 \(1\) 到 \(n\)),每种颜色的小球他都有无限多个。他还有 \(n\) 个球桶(编号 \(1\) 到 \(n\)),球桶的内径与小球直径相当且高度是无限大的,因此能容纳无限多的小球。他想用这些小球和球桶玩游戏。
一开始这些球桶都是空的,紧接着他会顺次地做 \(m\) 个操作,每个操作都是以下 \(3\) 类操作中的一个。
- \(push\:x\:y\:z\):把 \(x\) 个颜色为 \(y\) 的彩色小球放到第 \(z\) 个桶的最上面;
- \(pop\:x\:z\):把最上面的 \(x\) 个小球从第 \(z\) 个桶内拿出来;
- \(put\:u\:v\):把第 \(u\) 个桶的所有小球依次从顶部拿出放入第 \(v\) 个桶内。
现在他已经确定好了这 \(m\) 个操作,但在他开始玩之前,他想知道每次他进行第二类操作取出的最后一个小球是什么颜色。
输入格式
第一行两个正整数 。
接下来 行每行是一个操作:
- 如果为第一类操作,则格式为 \(push\:x\:y\:z\)(\(x,y,z\) 为正整数,\(1 \le x \le 10^9, 1 \le y,z \le n\));
- 如果为第二类操作,则格式为 \(pop\:x\:z\)(\(x, z\) 为正整数,\(1 \le x \le 10^9, 1 \le z \le n\))保证第 \(z\) 个桶内至少有 \(x\) 个小球;
- 如果为第三类操作,则格式为 \(put\:u\:v\)(\(u, v\) 为正整数,\(1 \le u,v \le n, u \not= v\)) 。
输出格式
对于每个第二类操作,输出牛牛取出的最后一个小球的颜色编号。
样例
- Input
2 5
push 1 1 1
push 1 2 1
push 2 3 2
put 1 2
pop 2 2
- Output
2
题解
感觉有点为了这碟醋包了一盘饺子。 主要就是展示一下三分裂的写法和用法。
std 给的链表做法,不过也可以用平衡树做,这里写 FHQ Treap 展示三分裂。
你会发现这题就是平衡树模板题。操作一二就是插入删除,操作三就是区间翻转。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn = 200005;
ll n, q;
struct Treap {
ll lson, rson;
ll pri, tag;
ll size, num, col;
// num 表示一次 push 进来的球数,col 表示这次 push 进来的球的颜色
// size 表示整个子树一共有多少球
} tree[maxn];
ll rootid[maxn], idcnt;
inline ll newNode(ll color, ll number) {
++idcnt;
tree[idcnt].lson = tree[idcnt].rson = 0;
tree[idcnt].size = tree[idcnt].num = number;
tree[idcnt].col = color;
tree[idcnt].pri = rand();
tree[idcnt].tag = 0;
return idcnt;
}
inline void update(ll u) {
tree[u].size = tree[u].num + tree[tree[u].lson].size + tree[tree[u].rson].size;
}
inline void pushdown(ll u) {
if (!tree[u].tag) return;
ll l = tree[u].lson, r = tree[u].rson;
if (l) tree[l].tag ^= 1, swap(tree[l].lson, tree[l].rson);
if (r) tree[r].tag ^= 1, swap(tree[r].lson, tree[r].rson);
tree[u].tag ^= 1;
}
// 三分裂,按排名分裂
inline void split(ll u, ll k, ll &L, ll &M, ll &R) {
if (!u) return L = M = R = 0, void();
pushdown(u);
// 注意我们是要取最后 k 个,因此 tree[tree[u].rson].size >= k 要向右子树递归。
if (tree[tree[u].rson].size >= k) {
L = u;
split(tree[u].rson, k, tree[u].rson, M, R);
} else if (tree[tree[u].rson].size + tree[u].num < k) {
// 如果右子树加上 u 本身的球数还不达 k 个,则向左子树递归。
R = u;
split(tree[u].lson, k - tree[tree[u].rson].size - tree[u].num, L, M, tree[u].lson);
} else {
// 如果恰好等于,则可以直接停止递归,将树分成 L,M,R 三棵。
L = tree[u].lson, R = tree[u].rson, M = u;
tree[u].lson = tree[u].rson = 0;
}
update(u);
}
ll merge(ll L, ll R) {
if (!L || !R) return L + R;
pushdown(L), pushdown(R);
if (tree[L].pri > tree[R].pri) {
tree[L].rson = merge(tree[L].rson, R);
update(L);
return L;
} else {
tree[R].lson = merge(L, tree[R].lson);
update(R);
return R;
}
return 0;
}
// 非常方便且快速地把我们需要的等于 k 的节点提出来了
// 而不用先分成两棵树,然后再单独提
inline ll delNode(ll u, ll k) {
ll L = 0, M = 0, R = 0;
split(rootid[u], k, L, M, R);
// 删去最后 k 个,然而 tree[R].size + tree[M].num 可能是大于 k 个的。
// 所以需要再建一个点把不该删除的部分给加回来,颜色为 M 的颜色,显然。
rootid[u] = merge(L, newNode(tree[M].col, tree[R].size + tree[M].num - k));
return tree[M].col;
}
signed main() {
// freopen("color.in", "r", stdin);
// freopen("color.out", "w", stdout);
srand(time(NULL));
scanf("%lld%lld", &n, &q);
while (q--) {
char opt[10];
ll x, y, z, u, v;
scanf("%s", opt);
if (!strcmp(opt, "push")) {
scanf("%lld%lld%lld", &x, &y, &z);
// 直接新建一个点表示 x 个颜色为 y 的彩色小球,然后合并
rootid[z] = merge(rootid[z], newNode(y, x));
} else if (!strcmp(opt, "pop")) {
scanf("%lld%lld", &x, &z);
printf("%lld\n", delNode(z, x));
} else {
scanf("%lld%lld", &u, &v);
// 基本区间翻转
tree[rootid[u]].tag ^= 1;
swap(tree[rootid[u]].lson, tree[rootid[u]].rson);
rootid[v] = merge(rootid[v], rootid[u]);
rootid[u] = 0;
}
}
return 0;
}

浙公网安备 33010602011771号