浅谈平衡树
平衡树,是一种数据结构,可以实现一类元素在线性结构中动态变化,基于二叉搜索树,满足二叉搜索树的所有性质。
二叉搜索树(BST)
二叉搜索树是一种二叉树形结构,它满足以下性质:
-
空树是二叉搜索树。
-
若二叉搜索树的左子树不为空,则其左子树上所有点的附加权值均小于其根节点的值。
-
若二叉搜索树的右子树不为空,则其右子树上所有点的附加权值均大于其根节点的值。
-
二叉搜索树的左右子树均为二叉搜索树。
简单来讲,就是 左 < 中 < 右。
二叉搜索树上的基本操作所花费的时间与这棵树的高度成正比。对于一个有 n 个结点的二叉搜索树中,这些操作的最优时间复杂度为
平衡树
平衡树,字面意思,就是平衡的树。它是一种优化的二叉搜索树,能够使二叉搜索树的深度尽量小,使树趋于平衡,从而优化操作的时间复杂度。
在这里,平衡指所有叶子的深度趋于平衡,更广义的是指在树上所有可能查找的均摊复杂度偏低。[2]
几乎所有平衡树的实现方法都是基于树旋转操作[3],但由于笔者水平有限,本文仅介绍一种相对简单,且无需旋转的平衡树——无旋 Treap。
Treap
Treap 是一种弱平衡二叉搜索树,它同时满足二叉搜索树和堆的性质,因此得名也为 Tree(树) 和 Heap(堆)的组合。
在 Treap 中,我们引入了一个随机附加域,每一个结点有一个随机的附加值,满足堆的性质,结点原权值满足二叉搜索树的性质。
下文称随机附加值为
使用一个随机附加域来满足堆的性质,可以使 Treap 处于一个期望的平衡状态,使 Treap 单次操作的期望复杂度为
首先,我们需要认识到一个节点的
是和它所在的层数有直接关联的。再回忆堆的性质:
- 子节点
比父节点大或小(取决于是小根堆还是大根堆) 我们发现层数低的节点,比如整个树的根节点,它的
也会更小(在小根堆中)。并且,在朴素的搜索树中,先被插入的节点,也更有可能会有比较小的层数。我们可以把这个 和被插入的顺序关联起来理解,这样,也就理解了为什么 Treap 可以把节点插入的顺序通过随机附加域打乱。
无旋 Treap
无旋 Treap,是一种无需旋转,且满足 Treap 性质的平衡树。
它的核心操作有两种,分别是分裂和合并。通过这两种操作,无旋 Treap 可以比有旋 Treap 更灵活、方便地实现操作。无旋 Treap 的大部分操作都基于这两个操作。
分裂(Split)
分裂操作将一棵 Treap 分裂成两颗树
这样我们就可以使用分裂出来的两棵树,依靠上面的性质进行各种操作。
至于具体的分裂过程,同样依靠二叉搜索树的性质,进行递归分裂。对于当前搜到的结点
void split(int cur, int &a, int &b, int val) //此处的 val 即为 key;这里的a、b加取址符是为了方便将a与b的内部连接 { if(!cur)//叶子结点 return a = b = 0, void();//停止分裂 if(tree[cur].val <= val)//val小于等于key a = cur, split(rs, rs, b, val);//左子树与cur均属于a,继续分裂右子树 else//val > key b = cur, split(ls, a, ls, val);//右子树与cur均属于a,继续分裂左子树 pushup(cur);//更新当前结点信息 return; }
合并(Merge)
合并操作将两棵 Treap 合并。设它们的根分别为
由于维护的是平衡,所以需要考虑哪一棵放在上面,哪一棵放在下面作另一棵的子树。考虑递归逐层合并,设当前两棵树合并后的根为
void merge(int &cur, int a, int b) //cur加取址符是为了方便直接取出合并后的根 { if(!a || !b) return cur = a + b, void(); if(tree[a].rnk < tree[b].rnk)//a在b上方 cur = a, merge(rs, rs, b);//b与a的右儿子继续合并,争夺a右儿子 else//b在a上方 cur = b, merge(ls, a, ls);//a与b的左儿子继续合并,争夺b左儿子 pushup(cur);//更新当前结点信息 return; }
插入(Insert)
无旋 Treap 的插入、删除等基本操作都可以利用分裂和合并简短地实现。
根据分裂操作的性质,分裂出来的两颗 Treap 一棵小于等于
设要插入的点权值为
新建一个结点
将
void insert(int &cur, int val)//由于分裂、合并后根可能会变,因此cur加取址符 { int a = 0, b = 0, c = add_node(val);//新建一个权值为val的结点 split(cur, a, b, val);//按照val分裂为a,b merge(a, a, c), merge(cur, a, b);//将a,c合并,再与b合并 return; }
删除(Delete)
删除与插入类似,用分裂找到权值等于
void delate(int &cur, int val) { int a = 0, b = 0, c = 0; split(cur, a, b, val), split(a, a, c, val - 1);//a树<val,c树=val,b树>val merge(c, tree[c].lt, tree[c].rt), merge(a, a, c), merge(cur, a, b);//根据题目要求判断删一个还是全删,此处只删一个 //c树中的结点都=val,这里是将根节点c的左右子树合并,那么c就删掉了 return; }
根据值查询排名(Query Ranking)
这里的排名定义为比当前数小的数的个数
将树分裂为小于
当排名的定义有变化时,方法也需要变化。
int find_rnk(int &cur, int x) { int a = 0, b = 0; split(cur, a, b, x - 1);//a树<=x-1,b树>x-1,也就是a树<x,b树>=x int ans = tree[a].siz + 1;//排名就是a的大小+1 merge(cur, a, b); return ans; }
根据排名查询值(Query Number)
这个操作并不建议使用分裂与合并,因为需要根据树的大小分裂。
所以使用更为简单的 dfs。由于 BST 的性质,左右子树有序,所以可以根据左子树的大小来判断排名。
int find_num(int cur, int x) { if(tree[ls].siz + 1 == x)//比cur小的结点数+1为x,说明cur的排名为x return tree[cur].val; if(tree[ls].siz < x)//比cur小的结点数小于x,说明排名还要变大,查询右子树 return find_num(rs, x - tree[ls].siz - 1);//注意这里要将左子树的大小与cur剪掉 else//反之则是说明排名还要减小,查询左子树 return find_num(ls, x); }
查询前驱(Query Previous)
前驱定义为小于 find_num()
来根据排名查询值。
int find_prev(int &cur, int val) { int a = 0, b = 0; split(cur, a, b, val - 1);//树a<val,树b>=val int ans = find_num(a, tree[a].siz);//a的大小就是val的前驱的排名,直接查询 merge(cur, a, b); return ans; }
查询后继(Query Next)
后继定义为大于 find_num()
查找。
int find_next(int &cur, int val) { int a = 0, b = 0; split(cur, a, b, val);//树a<=val,树b>val int ans = find_num(b, 1);//b中的最小的数就是比val大的第一个数 merge(cur, a, b); return ans; }
模板题 P3369 【模板】普通平衡树
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
- 插入一个数
。 - 删除一个数
(若有多个相同的数,应只删除一个)。 - 定义排名为比当前数小的数的个数
。查询 的排名。 - 查询数据结构中排名为
的数。 - 求
的前驱(前驱定义为小于 ,且最大的数)。 - 求
的后继(后继定义为大于 ,且最小的数)。
对于操作 3,5,6,不保证当前数据结构中存在数
对于
解法
这道题的操作就是上面分析的所有操作,整合一下即可。
Code
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 1e5 + 5; int n, num, root; struct Tree { int lt, rt, val, siz, rnk; }tree[N]; mt19937 rnd(time(0)); #define ls tree[cur].lt #define rs tree[cur].rt void pushup(int cur) { tree[cur].siz = tree[ls].siz + tree[rs].siz + 1; return; } void split(int cur, int &a, int &b, int val) //此处的 val 即为 key;这里的a、b加取址符是为了方便将a与b的内部连接 { if(!cur)//叶子结点 return a = b = 0, void();//停止分裂 if(tree[cur].val <= val)//val小于等于key a = cur, split(rs, rs, b, val);//左子树与cur均属于a,继续分裂右子树 else//val > key b = cur, split(ls, a, ls, val);//右子树与cur均属于a,继续分裂左子树 pushup(cur);//更新当前结点信息 return; } void merge(int &cur, int a, int b) //cur加取址符是为了方便直接取出合并后的根 { if(!a || !b) return cur = a + b, void(); if(tree[a].rnk < tree[b].rnk)//a在b上方 cur = a, merge(rs, rs, b);//b与a的右儿子继续合并,争夺a右儿子 else//b在a上方 cur = b, merge(ls, a, ls);//a与b的左儿子继续合并,争夺b左儿子 pushup(cur);//更新当前结点信息 return; } int add_node(int val)//新建结点 { return tree[++num] = {0, 0, val, 1, (int)rnd()}, num; } void insert(int &cur, int val)//由于分裂、合并后根可能会变,因此cur加取址符 { int a = 0, b = 0, c = add_node(val);//新建一个权值为val的结点 split(cur, a, b, val);//按照val分裂为a,b merge(a, a, c), merge(cur, a, b);//将a,c合并,再与b合并 return; } void delate(int &cur, int val) { int a = 0, b = 0, c = 0; split(cur, a, b, val), split(a, a, c, val - 1);//a树<val,c树=val,b树>val merge(c, tree[c].lt, tree[c].rt), merge(a, a, c), merge(cur, a, b);//根据题目要求判断删一个还是全删,此处只删一个 //c树中的结点都=val,这里是将根节点c的左右子树合并,那么c就删掉了 return; } int find_rnk(int &cur, int x) { int a = 0, b = 0; split(cur, a, b, x - 1);//a树<=x-1,b树>x-1,也就是a树<x,b树>=x int ans = tree[a].siz + 1;//排名就是a的大小+1 merge(cur, a, b); return ans; } int find_num(int cur, int x) { if(tree[ls].siz + 1 == x)//比cur小的结点数+1为x,说明cur的排名为x return tree[cur].val; if(tree[ls].siz < x)//比cur小的结点数小于x,说明排名还要变大,查询右子树 return find_num(rs, x - tree[ls].siz - 1);//注意这里要将左子树的大小与cur剪掉 else//反之则是说明排名还要减小,查询左子树 return find_num(ls, x); } int find_prev(int &cur, int val) { int a = 0, b = 0; split(cur, a, b, val - 1);//树a<val,树b>=val int ans = find_num(a, tree[a].siz);//a的大小就是val的前驱的排名,直接查询 merge(cur, a, b); return ans; } int find_next(int &cur, int val) { int a = 0, b = 0; split(cur, a, b, val);//树a<=val,树b>val int ans = find_num(b, 1);//b中的最小的数就是比val大的第一个数 merge(cur, a, b); return ans; } signed main() { cin >> n; while(n--) { int opt, x; cin >> opt >> x; if(opt == 1) insert(root, x); else if(opt == 2) delate(root, x); else if(opt == 3) cout << find_rnk(root, x) << "\n"; else if(opt == 4) cout << find_num(root, x) << "\n"; else if(opt == 5) cout << find_prev(root, x) << "\n"; else cout << find_next(root, x) << "\n"; } return 0; }
序列上的无旋 Treap
无旋 Treap 是一种可以维护序列操作的平衡树。
建树
在序列无旋 Treap 中,分裂操作与普通无旋 Treap 不同。在这里,我们不按照权值
于是在分裂操作中,每个点按照左子树的大小分裂,由于 BST 性质,这样就可以保证 Treap 的中序遍历为原序列的顺序。
插入时,若插入结点在原序列中的编号为
void split(int cur, int &a, int &b, int val) { if(!cur) return a = b = 0, void(); if(tree[ls].siz + 1 <= val) a = cur, split(rs, rs, b, val - tree[ls].siz - 1);//注意这里要减去cur与左子树的大小 else b = cur, split(ls, a, ls, val); return pushup(cur); } void merge(int &cur, int a, int b) { if(!a || !b) return cur = a + b, void(); if(tree[a].rnk < tree[b].rnk) cur = a, merge(rs, rs, b); else cur = b, merge(ls, a, ls); return pushup(cur); } int add_node(int val) { tree[++num] = {0, 0, val, 1, 0, (int)rnd()}; return num; } void insert(int &cur, int val, int id) { int a = 0, b = 0, c = add_node(val); split(cur, a, b, id); merge(a, a, c), merge(cur, a, b); return; }
模板题 P3391 【模板】文艺平衡树
您需要写一种数据结构(可参考题目标题),来维护一个有序数列。
其中需要提供以下操作:翻转一个区间,例如原有序序列是
对于
解法
这是一个序列问题,所以要用维护序列的平衡树。
对于区间翻转操作,由于树中每一个结点的中序遍历都是原序列中的一段区间,那么它的左右子节点也表示一段区间,并且合并后也是一段区间,所以只需要分裂出需要修改的区间,交换所有结点左右子节点即可。
但是这样复杂度显然不能接受,于是可以类似线段树,对结点打懒标记,这样复杂度即可接收。
Code
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 1e5 + 5; int n, m, num, root; struct Tree { int lt, rt, val, siz, tag, rnk; }tree[N]; mt19937 rnd(time(0)); #define ls tree[cur].lt #define rs tree[cur].rt void pushup(int cur) { tree[cur].siz = tree[ls].siz + tree[rs].siz + 1; return; } void pushdown(int cur)//标记下传 { if(!tree[cur].tag) return; swap(ls, rs); tree[ls].tag ^= 1, tree[rs].tag ^=1; tree[cur].tag = 0; return; } void split(int cur, int &a, int &b, int val) { if(!cur) return a = b = 0, void(); pushdown(cur);//标记下传 if(tree[ls].siz + 1 <= val) a = cur, split(rs, rs, b, val - tree[ls].siz - 1); else b = cur, split(ls, a, ls, val); return pushup(cur); } void merge(int &cur, int a, int b) { if(!a || !b) return cur = a + b, void(); pushdown(a), pushdown(b);//标记下传 if(tree[a].rnk < tree[b].rnk) cur = a, merge(rs, rs, b); else cur = b, merge(ls, a, ls); return pushup(cur); } int add_node(int val) { tree[++num] = {0, 0, val, 1, 0, (int)rnd()}; return num; } void insert(int &cur, int val) { int a = 0, b = 0, c = add_node(val); merge(cur, cur, c); return; } void update(int &cur, int l, int r) { int a = 0, b = 0, c = 0; split(cur, a, b, r), split(a, a, c, l - 1);//分裂出修改的区间 tree[c].tag ^= 1;//打标记 merge(a, a, c), merge(cur, a, b); return; } void dfs(int cur) { if(!cur) return; pushdown(cur); dfs(ls); cout << tree[cur].val << " "; dfs(rs); return; } signed main() { cin >> n >> m; for(int i = 1; i <= n; i++) insert(root, i); while(m--) { int l, r; cin >> l >> r; update(root, l, r); } dfs(root); return 0; }
本文作者:Luckies
本文链接:https://www.cnblogs.com/Luckies/p/18199309/treap
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步