回归数据结构了,第一个想起幼时噩梦平衡树。
题面:
我们需要一种数据结构,
支持插入、删除、按位查询、查询前驱后继、查询比固定值小的值数量等操作。
平衡树:树上任一结点的左子树和右子树的高度之差不超过 1,两个子树“平衡”的树结构。
在平衡树上执行一次操作,时间复杂度一般为 。
(不“平衡”的平衡树一次操作会退化为 )
解析:
我个人推荐两种方法,初学者使用 FHQ Treap,可了解算法底层逻辑。
pbds 码量很小,并且 noi 系列比赛明确可以使用,也有必要学。
(这篇先讲 FHQ,pbds 的代码放在这里)
其它的不是比 Treap 码量大,就是不如这俩快,可以不学。
(1)FHQ Treap
FHQ Treap(无旋 Treap)是由清华大学的范浩强提出的一种平衡树实现方式。
相较于 Splay(伸展树),它不使用旋转操作来维持平衡。
(可以理解 Splay 是有点被淘汰的平衡树算法,有多而复杂的旋转操作。
它通过每次询问都将被询问点不断“伸展”,向上旋转至根节点。
来保持二叉查找树性质,实现自适应性,维持均摊平衡。
但在动态树算法方面,Splay 是不可或缺的,我先挖个坑以后讲)
而是通过两个核心操作:split(分割)和 merge(合并)来实现所有功能。
Treap 节点结构体定义:
Treap 是 Tree 和 Heap 的组合,既满足二叉搜索树性质也满足堆性质。
二叉搜索树性质: 左子树中所有节点的值都小于该节点的值,
右子树中所有节点的值都大于该节点的值,左右子树也都是二叉搜索树。
(最大)堆性质:对于树中任意节点,其值都大于或等于其子节点的值,
且根节点是树中的最大值。
#define lc(p) tr[p].ls //方便调用左右节点的宏定义,我个人码风,可以不加
#define rc(p) tr[p].rs
const int N = 1e5 + 10;
struct node {
int ls, rs; // 左右子节点
int val; // 节点值:用于维护二叉搜索树性质,保证树的平衡,分割操作的时候会用到
int siz; // 以该节点为根的子树大小
int rnd; // 随机优先级:用于维护堆性质,保证树的平衡,合并操作的时候会用到
} tr[N];
int rt, trlen; // rt为根节点,trlen为已分配节点数
普通操作函数:
// 创建新节点:初始化
int newd(int v) {
trlen++;
tr[trlen] = {0, 0, v, 1, rand()}; //子树大小为 1,随机优先级
return trlen;
}
// 更新节点p的子树大小
void pushup(int p) {
tr[p].siz = tr[lc(p)].siz + tr[rc(p)].siz + 1;
}
关于优先级为什么是随机的这回事:
如果你按顺序插入序列 ,按二叉搜索树性质,就会变成这样:
这是一条链,查找起来是 的,也就是不平衡。
但如果我们加上随机优先级,假设优先级对应是这样的:。
那么就有如下流程(更具体的合并操作请看 merge 部分代码):
(图片来源:董晓算法)
可以达成平衡。
split 操作(分割):
Treap 的一个核心操作。
将一棵树按 val 值分割成两棵树,分割操作完成后 x 是左子树,y 是右子树。
并且 lc(p) 所有的值 <= tr[p].val <= rc(p) 所有的值。
递归过程中,有:
x(是所有节点值 ≤ val)的子树的根
y(是所有节点值 > val)的子树的根
代码:
// p是要被分割的子树根,按 v 值分割,x 和 y 一开始传进来是啥也没有的,需要在函数中一次次更新
void split(int p, int v, int &x, int &y) {
if (p == 0) { // 空树情况
x = y = 0;
return;
}
if (tr[p].val <= v) { //说明 lc(p) 可以整个归到左子树
x = p; //先把左子树定位 p,递归完成回溯时 p里面的值都小于等于 v
split(rc(p), v, rc(x), y);
//接下来分割以 rc(p) 为根的子树,因为 lc(p)所有的值 <= v,但是 rc(p) 只有一部分 <=v
}
else { //说明 rc(p) 可以整个归到右子树
y = p; //先把右子树定位 p,递归完成回溯时 p里面的值都大于 v
split(lc(p), v, x, lc(y));
//接下来分割以 lc(p) 为根的子树,因为 rc(p)所有的值 > v,但是 lc(p) 只有一部分 >v
}
pushup(p); // 更新 p 节点信息
}
merge 操作(合并):
这是 Treap 的另一个核心操作,与 split 互为逆操作。
合并两棵树 x 和 y,因为代码实现中都是 split 后面紧跟着 merge,
就能保证 x 中所有节点值 <= y 中所有节点值。
代码:
int merge(int x, int y) {
if ( (!x) || (!y) ) return x + y; //空树情况,当 x和 y其中一方为 0,那输出另一方
// 优先级 rnd 较小的节点作为根,保证树的平衡
if (tr[x].rnd < tr[y].rnd) {
rc(x) = merge(rc(x), y); // 将 y合并到 x的右子树
pushup(x);
return x;
}
else {
lc(y) = merge(x, lc(y)); // 将 x合并到 y的左子树
pushup(y);
return y;
}
}
实现操作函数:
插入与删除:
// 插入值 v
void ins(int v) {
int x, y;
split(rt, v, x, y); // 将树按 v 分割成 x(y<=v) 和 y(y>v)
rt = merge( merge( x, newd(v) ), y ); // 在 x 和 y 之间插入新节点后合并
//你愿意的话这么写也没问题:rt = merge( x, merge( newd(v), y ) );
}
// 删除值 v(只删除一个匹配项)
void del(int v)
{
int x, y, z;
split(rt, v-1, x, y); // 分割出 x(<=v-1) 和 y(>=v)
split(y, v, y, z); // 将 y 分割成 y(=v) 和 z(>v)
//合并 y 的左右子树(即删除 y 的根节点),然后与 x 和 z 合并
rt = merge( merge( x, merge( lc(y), rc(y) ) ), z );
}
获取排名:
//获取值 v 的排名(即 v 是第几小的元素)
int getrnk(int v) {
int x, y;
split(rt, v-1, x, y); // 分割出 x(<=v-1) 和 y(>=v),因为有多个重复值只能这么做
int res = tr[x].siz+1; // x 的大小 + 1 即为 v 的排名
rt = merge(x, y); //合并(恢复)原树
return res;
}
// 获取排名为 k 的元素值
int getval(int p, int k) {
if ( k == tr[lc(p)].siz + 1 ) {
return tr[p].val; // 正好是当前节点
}
if ( k <= tr[lc(p)].siz ) {
return getval(lc(p), k); // 在左子树中
}
else {
return getval(rc(p), k - tr[lc(p)].siz - 1); // 在右子树中
}
}
前驱后继:
// 获取v的前驱(小于 v 的最大值)
int getpre(int v) {
int x, y;
split(rt, v - 1, x, y); // 分割出 x(<=v-1) 和 y(>=v)
int res = getval(x, tr[x].siz); // 获取 x 中排名为 tr[x].siz 的元素(即最大值)
rt = merge(x, y); // 恢复原树
return res;
}
// 获取v的后继(大于v的最小值)
int getnxt(int v) {
int x, y;
split(rt, v, x, y); // 分割出 x(<=v)和 y(>v)
int res = getval(y, 1); // 获取 y 中排名为 1 的元素(即最小值)
rt = merge(x, y); // 恢复原树
return res;
}
洛谷 P3369 代码
主函数就不单独放了,直接上整代码(无注释,方便调式):
#include<bits/stdc++.h>
using namespace std;
#define lc(p) tr[p].ls
#define rc(p) tr[p].rs
const int N = 1e5 + 10;
struct node {
int ls, rs;
int val;
int siz;
int rnd;
} tr[N];
int rt, trlen;
int newd(int v) {
trlen++;
tr[trlen] = {0, 0, v, 1, rand()};
return trlen;
}
void pushup(int p) {
tr[p].siz = tr[lc(p)].siz + tr[rc(p)].siz + 1;
}
void split(int p, int v, int &x, int &y) {
if (p == 0) {
x = y = 0;
return ;
}
if (tr[p].val <= v) {
x = p;
split(rc(p), v, rc(x), y);
}
else {
y = p;
split(lc(p), v, x, lc(y));
}
pushup(p);
}
int merge(int x, int y) {
if ( (!x) || (!y) ) {
return x + y;
}
if (tr[x].rnd < tr[y].rnd) {
rc(x) = merge(rc(x), y);
pushup(x);
return x;
}
else {
lc(y) = merge(x, lc(y));
pushup(y);
return y;
}
}
void ins(int v) {
int x, y;
split(rt, v, x, y);
rt = merge( merge( x, newd(v) ), y );
}
void del(int v) {
int x, y, z;
split(rt, v - 1, x, y);
split(y, v, y, z);
rt = merge( merge( x, merge( lc(y), rc(y) ) ), z );
}
int getrnk(int v) {
int x, y;
split(rt, v - 1, x, y);
int res = tr[x].siz + 1;
rt = merge(x, y);
return res;
}
int getval(int p, int k) {
if (tr[lc(p)].siz + 1 == k) {
return tr[p].val;
}
if (k <= tr[lc(p)].siz) {
return getval(lc(p), k);
}
else {
return getval(rc(p), k - tr[lc(p)].siz - 1);
}
}
int getpre(int v) {
int x, y;
split(rt, v - 1, x, y);
int res = getval(x, tr[x].siz);
rt = merge(x, y);
return res;
}
int getnxt(int v) {
int x, y;
split(rt, v, x, y);
int res = getval(y, 1);
rt = merge(x, y);
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
rt = trlen = 0;
for (int i = 1; i <= n; i++) {
int opt, x;
cin >> opt >> x;
if (opt == 1) {
ins(x);
}
else if (opt == 2) {
del(x);
}
else if (opt == 3) {
cout << getrnk(x) << "\n";
}
else if (opt == 4) {
cout << getval(rt, x) << "\n";
}
else if (opt == 5) {
cout << getpre(x) << "\n";
}
else if (opt == 6) {
cout << getnxt(x) << "\n";
}
}
return 0;
}
(2)可持久化 FHQ Treap
可持久化数据结构:是指能够保留历史版本的数据结构。
每次修改操作后,原版本仍然可用,新操作基于原版本创建新版本。
并且每次开新节点只会记录与前一个版本不一样的结点,也就是动态开点。
强烈推荐董晓老师的这个视频!!!看完保证全懂!!
我这里就只放代码。
洛谷 P3835 代码:
#include<bits/stdc++.h>
using namespace std;
#define lc(p) tr[p].ls
#define rc(p) tr[p].rs
const int N = 5e5 + 10;
struct node {
int ls, rs;
int val;
int siz;
int rnd;
} tr[N * 50];
int root[N], trlen;
int newd(int v) {
trlen++;
tr[trlen] = {0, 0, v, 1, rand()};
return trlen;
}
void pushup(int p) {
tr[p].siz = tr[lc(p)].siz + tr[rc(p)].siz + 1;
}
void split(int p, int v, int &x, int &y) {
if (p == 0) {
x = y = 0;
return ;
}
if (tr[p].val <= v) {
trlen++;
x = trlen; //这里一定不能这么写:x = trlen++; 因为要先自增再赋值
tr[x] = tr[p];
split(rc(p), v, rc(x), y);
pushup(x);
}
else {
trlen++;
y = trlen;
tr[y] = tr[p];
split(lc(p), v, x, lc(y));
pushup(y);
}
}
int merge(int x, int y) {
if ( (!x) || (!y) ) {
return x + y;
}
if (tr[x].rnd < tr[y].rnd) {
rc(x) = merge(rc(x), y);
pushup(x);
return x;
}
else {
lc(y) = merge(x, lc(y));
pushup(y);
return y;
}
}
void ins(int &rt, int v) {
int x, y;
split(rt, v, x, y);
rt = merge( merge( x, newd(v) ), y );
}
void del(int &rt, int v) {
int x, y, z;
split(rt, v - 1, x, y);
split(y, v, y, z);
rt = merge( merge( x, merge( lc(y), rc(y) ) ), z );
}
int getrnk(int &rt, int v) {
int x, y;
split(rt, v - 1, x, y);
int res = tr[x].siz + 1;
rt = merge(x, y);
return res;
}
int getval(int p, int k) {
if (tr[lc(p)].siz + 1 == k) {
return tr[p].val;
}
if (k <= tr[lc(p)].siz) {
return getval(lc(p), k);
}
else {
return getval(rc(p), k - tr[lc(p)].siz - 1);
}
}
int getpre(int &rt, int v) {
int x, y;
split(rt, v - 1, x, y);
if (!x) {
return -2147483647;
}
int res = getval(x, tr[x].siz);
rt = merge(x, y);
return res;
}
int getnxt(int &rt, int v) {
int x, y;
split(rt, v, x, y);
if (!y) {
return 2147483647;
}
int res = getval(y, 1);
rt = merge(x, y);
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
root[0] = trlen = 0;
for (int i = 1; i <= n; i++) {
int v, opt, x;
cin >> v >> opt >> x;
root[i] = root[v];
if (opt == 1) {
ins(root[i], x);
}
else if (opt == 2) {
del(root[i], x);
}
else if (opt == 3) {
cout << getrnk(root[i], x) << "\n";
}
else if (opt == 4) {
cout << getval(root[i], x) << "\n";
}
else if (opt == 5) {
cout << getpre(root[i], x) << "\n";
}
else if (opt == 6) {
cout << getnxt(root[i], x) << "\n";
}
}
return 0;
}