Treap 平衡树
\(Treap\) 平衡树
概念
\(Treap\) 是一种代码复杂度与思维都较为简单的平衡树,它在普通 \(BST\) 的基础上,给每一个节点赋予了一个优先级属性。对于 \(Treap\) 中的每一个节点,除了它的权值满足 \(BST\) 的基本关系,它的优先级也满足小根堆的性质。
我们发现 \(BST\) 退化为链的原因是因为插入的数据有序(所有数据均被插在左子树或右子树),我们想办法随机构造优先级,这样它就无序了。
所以,\(Treap=Tree+heap\)。(本文为小根堆)
基本操作
旋转
为了保证 \(Treap\) 在满足 \(BST\) 的基础上又满足小根堆的性质,我们每次操作都需要对它进行调整,而调整的方法是旋转:
-
左旋(\(Zag\)):左旋一棵子树(根为 \(u\)),我们可以把它理解为将该子树的左子树的右子树提起来,然后对下方节点特殊处理
graph TB 1((1)) 2((2)) 3((3)) 4((4)) 5((5)) 4---2 4---5 2---1 2---3 a1((1)) a2((2)) a3((3)) a4((4)) a5((5)) a2---a1 a2---a4 a4---a3 a4---a5旋转后 \(u\) 会变为该子树的新根的右子节点,\(u\) 的左子节点变为新根
-
右旋(\(Zig\)):右旋一棵子树(根为 \(u\)),我们可以把它理解为将该子树的右子树的右子树提起来,然后对下方节点特殊处理
旋转后 \(u\) 会变为该子树的新根的左子节点,\(u\) 的右子节点变为新根
显然旋转不会使 \(Treap\) 失去 \(BST\) 的性质,我们可以通过旋转将 \(BST\) 变为 \(Heap\)
struct node {
int lson,rson; // 左右子节点
int val,pri; // 权值与优先级
int cnt; // 计数器,重复的元素
int size; // 子树节点个数(后面其它操作需要)
};
node tree[maxn]; // Treap
// 注意地址引用&!
inline void zig(int &uid) { // 右旋
node &u = tree[uid];
node &v = tree[u.lson]; int vid = u.lson;
u.lson = v.rson; // 更新节点关系
v.rson = uid;
u.size = tree[u.lson].size + tree[u.rson].size + u.cnt; // 更新大小
v.size = tree[v.lson].size + tree[v.rson].size + v.cnt;
uid = vid; // 更新根节点
}
inline void zag(int &uid) { // 左旋
node& u = tree[uid];
node& v = tree[u.rson]; int vid = u.rson;
u.rson = v.lson; // 更新节点关系
v.lson = uid;
u.size = tree[u.lson].size + tree[u.rson].size + u.cnt; // 更新大小
v.size = tree[v.lson].size + tree[v.rson].size + v.cnt;
uid = vid; // 更新根节点
}
注意:注意使用引用&!我就是这样错的,呜呜呜……
插入节点
\(Treap\) 中插入节点与 \(BST\) 中类似:
插入:
- 从根节点开始插入
- 若插入值小于(等于)当前值,则在当前节点的左子树中寻找插入位置
- 若插入值大于当前值,则在当前节点的右子树中寻找插入位置
- 若当前值为空节点,则在此建立空节点,为该节点生成一个随机的优先值
回溯(维护 \(Heap\) 的性质):
- 若左子节点的优先级小于当前节点的优先级,对以该节点为根的子树右旋
- 若右子节点的优先级小于当前节点的优先级,对以该节点为根的子树左旋(仅可能存在一个子节点小于的情况,再插入前保证本身就是一个 \(Heap\))
inline void insert(int &u,int val) {
if (!u) { // 空节点
u = ++ nodecnt; // 新节点编号
tree[u].val = val,tree[u].pri = rand(); // 权值与优先级
tree[u].cnt = tree[u].size = 1; // 重复元素数量与大小
tree[u].lson = tree[u].rson = 0; // 初始化左右子节点
return ;
}
tree[u].size ++; // 经过的每一个子树大小都需增加
if (tree[u].val == val) tree[u].cnt ++; // 相同节点
else if (val < tree[u].val) {
insert(tree[u].lson,val);
if (tree[tree[u].lson].pri < tree[u].pri) zig(u);
} else {
insert(tree[u].rson,val);
if (tree[tree[u].rson].pri < tree[u].pri) zag(u);
}
}
删除节点
\(Treap\) 中删除节点与 \(Heap\) 中类似:
- 该节点为叶子节点或链节点(只有一个子节点的节点),按照 \(BST\) 的方法直接删除
- 该节点为有两个子节点的节点,我们通过旋转将这个节点挤下去(可类比 \(Heap\) 中一步步将根比较下来),达成条件 \(1\) 后直接删除:
- 该节点左子节点优先级小于右子节点优先级,右旋该子树(将该节点降为右子节点)
- 该节点左子节点优先级大于右子节点优先级,左旋该子树(将该节点降为左子节点)
inline void deleteNode(int &u,int val) { // 删除权值为 val 的元素
if (tree[u].val == val) { // 找到节点
if (tree[u].cnt > 1) // 重复元素
tree[u].cnt --,tree[u].size --;
else if (!tree[u].lson || !tree[u].rson) // 链节点(叶节点)
u = (tree[u].lson ? tree[u].lson : tree[u].rson);
else if (tree[tree[u].lson].pri < tree[tree[u].rson].pri) { // 右旋
zig(u);
deleteNode(u,val); // 同层递归,类似循环这一步
} else { // 左旋
zag(u);
deleteNode(u,val);
}
return ;
}
tree[u].size --; // 类比插入操作第9行
if (val < tree[u].val) deleteNode(tree[u].lson,val);
else deleteNode(tree[u].rson,val);
}
求节点在 \(Treap\) 中的前驱
定义:在平衡树中不大于该元素的最大元素(类似二分查找或\(BST\)的元素查找)
实现方法:
- 从根节点开始访问(最初的答案为空节点)
- 如果当前节点的值不大于查询值,更新答案,并查找右子节点
- 如果当前节点的值大于查询值,查找左子节点
- 当前节点为空,结束查找
因为答案的更新在递归中实现麻烦,所以采用循环的方法:
inline int queryPre(int val) { // 求前驱
int u = root,ans = INT_MIN; // INT_MIN 只是用来判断该元素有没有前驱,可以根据题目取值
while (u) { // 非空节点
if (tree[u].val <= val) ans = tree[u].val,u = tree[u].rson;
else u = tree[u].lson;
}
return ans;
}
求节点在 \(Treap\) 中的后继
定义:在平衡树中不小于该元素的最大元素(与求前驱思路一致)
实现方法:
- 从根节点开始访问(最初的答案为空节点)
- 如果当前节点的值不小于查询值,更新答案,并查找左子节点
- 如果当前节点的值大于查询值,查找右子节点
- 当前节点为空,结束查找
因为答案的更新在递归中实现麻烦,所以采用循环的方法:
inline int querySuf(int val) { // 求后继
int u = root,ans = INT_MAX; // INT_MAX 只是用来判断该元素有没有前驱,可以根据题目取值
while (u) { // 非空节点
if (tree[u].val >= val) ans = tree[u].val,u = tree[u].lson;
else u = tree[u].rson;
}
return ans;
}
求在 \(Treap\) 中排名第 \(k\) 的元素
根据一条显而易见但非常重要的性质(\(Treap\) 中每一颗子树都是 \(Treap\)),我们显然可以想到利用分治的思想解决这个问题:
在一颗子树中,根 \(u\) 节点的排名取决于左子树的大小(左子树所有节点的值均小于 \(u\) ,则排名为 \(u.lson.size + 1\))。如果根有 \(cnt\) 个重复元素,那么 \(u\) 的排名为 \([u.lson.size+1,u.lson.size+u.cnt]\)。反着说就是:
- 当 \(k<u.lson.size+1\) 时,这个元素一定在左子树中
- 当 \(k>u.lson.size+u.cnt\) 时,这个元素一定在右子树中,且右子树中为排名第 \((k-(u.lson.size+u.cnt))\)(减去左子树与根的排名)的元素
- 当 \(k\in [u.lson.size+1,u.lson.size+u.cnt]\) 时,当前节点包含排名第 \(k\) 的元素
inline int queryKth(int k) {
int u = root;
while (u) {
int l = tree[tree[u].lson].size + 1,r = tree[tree[u].lson].size + tree[u].cnt;
if (l <= k && k <= r) return tree[u].val; // 当前节点
else if (k < l) u = tree[u].lson; // 左子树
else u = tree[u].rson,k -= r; // 右子树
}
return 0; // 没找到
}
求元素在 \(Treap\) 中的排名
注意:有重复元素时,根据题意判断排名是第一个数还是其它数(本文为第一个数)
求元素的排名,本质上就是求有多少个元素小于它,排名就是个数加一
令 \(u\) 为当前节点,\(ans\) 为答案(小于它的元素个数),则:
- 若查询值等于当前节点,返回答案为 \(res + u.lson.size + 1\)
- 若查询值小于当前节点,在左子树中寻找排名
- 若查询值大于当前节点,更新 \(res=res+u.lson.size+u.cnt\),在右子树中寻找排名
inline void queryRank(int val) {
int u = root,ans = 0;
while (u) {
if (val == tree[u].val) return ans + tree[tree[u].lson].size + 1;
else if (val < tree[u].val) u = tree[u].lson;
else ans += tree[tree[u].lson].size + tree[u].cnt,u = tree[u].rson;
}
return ans;
}

浙公网安备 33010602011771号