在夜空所有星星熄灭的时候,所有|

Luckies

园龄:2年粉丝:1关注:1

2024-07-25 12:49阅读: 12评论: 0推荐: 0

浅谈平衡树

平衡树,是一种数据结构,可以实现一类元素在线性结构中动态变化,基于二叉搜索树,满足二叉搜索树的所有性质。

二叉搜索树(BST)

二叉搜索树是一种二叉树形结构,它满足以下性质:

  1. 空树是二叉搜索树。

  2. 若二叉搜索树的左子树不为空,则其左子树上所有点的附加权值均小于其根节点的值。

  3. 若二叉搜索树的右子树不为空,则其右子树上所有点的附加权值均大于其根节点的值。

  4. 二叉搜索树的左右子树均为二叉搜索树。

简单来讲,就是 左 < 中 < 右

二叉搜索树上的基本操作所花费的时间与这棵树的高度成正比。对于一个有 n 个结点的二叉搜索树中,这些操作的最优时间复杂度为 O(logn),最坏为 O(n)。随机构造这样一棵二叉搜索树的期望高度为 O(logn)

平衡树

平衡树,字面意思,就是平衡的树。它是一种优化的二叉搜索树,能够使二叉搜索树的深度尽量小,使树趋于平衡,从而优化操作的时间复杂度。

在这里,平衡指所有叶子的深度趋于平衡,更广义的是指在树上所有可能查找的均摊复杂度偏低。

几乎所有平衡树的实现方法都是基于树旋转操作,但由于笔者水平有限,本文仅介绍一种相对简单,且无需旋转的平衡树——无旋 Treap。

Treap

Treap 是一种弱平衡二叉搜索树,它同时满足二叉搜索树和的性质,因此得名也为 Tree(树) 和 Heap(堆)的组合。

在 Treap 中,我们引入了一个随机附加域,每一个结点有一个随机的附加值,满足的性质,结点原权值满足二叉搜索树的性质。

下文称随机附加值为 rnk

使用一个随机附加域来满足堆的性质,可以使 Treap 处于一个期望的平衡状态,使 Treap 单次操作的期望复杂度为 O(logn)。由于笔者水平有限,并不清楚如何严格证明 Treap 的期望平衡,因此这里给出 OI Wiki 中的一种理解方式(略有改动):

首先,我们需要认识到一个节点的 rnk 是和它所在的层数有直接关联的。再回忆堆的性质:

  • 子节点 rnk 比父节点大或小(取决于是小根堆还是大根堆)

我们发现层数低的节点,比如整个树的根节点,它的 rnk 也会更小(在小根堆中)。并且,在朴素的搜索树中,先被插入的节点,也更有可能会有比较小的层数。我们可以把这个 rnk 和被插入的顺序关联起来理解,这样,也就理解了为什么 Treap 可以把节点插入的顺序通过随机附加域打乱。

无旋 Treap

无旋 Treap,是一种无需旋转,且满足 Treap 性质的平衡树。

它的核心操作有两种,分别是分裂合并。通过这两种操作,无旋 Treap 可以比有旋 Treap 更灵活、方便地实现操作。无旋 Treap 的大部分操作都基于这两个操作。

分裂(Split)

分裂操作将一棵 Treap 分裂成两颗树 a,b,一颗树的所有结点权值小于等于分裂的权值 key,一颗大于 key。因为二叉搜索树的性质,所以 a 树右下角的结点 keyb 树左下角的结点 >key,类似下图:

FHQ-Treap(1)

这样我们就可以使用分裂出来的两棵树,依靠上面的性质进行各种操作。

至于具体的分裂过程,同样依靠二叉搜索树的性质,进行递归分裂。对于当前搜到的结点 cur,设它的权值为 val,若 curkey,根据二叉搜索树的性质,左子树的结点权值均小于根节点,那么 cur 的左子树以及 cur 均属于 a 树,然后继续分裂右子树,反之亦然。下图可以更清晰地理解:

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 合并。设它们的根分别为 ab,那么 a 中所有点的权值必须小于等于 b 中的权值。在合并操作中,我们通过维护 rnk 的值来维护合并后 Treap 的平衡,不再维护 val,因此为了使合并后的 Treap 是 BST,a 中的权值必须小于等于 b 中的权值。

由于维护的是平衡,所以需要考虑哪一棵放在上面,哪一棵放在下面作另一棵的子树。考虑递归逐层合并,设当前两棵树合并后的根为 cur,根据堆的性质(小、大根堆皆可,此处为小根堆),将 rnk 小的放在 cur 的位置。若 rnka<rnkb,则 ab 上面, b 成为 a 的子树,并于 a 原来的子树继续合并。由于 a 的权值都小于 b,所以将 ba 的右子树继续合并,反之亦然。

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 一棵小于等于 key,一棵大于 key。设 a 为小于等于 key 的 Treap,那么 b 大于 val

设要插入的点权值为 val。将 Treap 按照 val 分裂,那么 a 中所有点权值都小于等于 val

新建一个结点 c,权值为 val。显然,一个单独的结点也是一颗 Treap。因为 a 中点权都小于等于 val,而 c 点权为 val,所以此时 ac 可以合并。

ac 合并,设根结点仍然为 a。此时 a 中点权仍然小于等于 val,所以仍然可以与 b 合并。ab 合并后的 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)

删除与插入类似,用分裂找到权值等于 val 的结点,并删除。但是需要注意题目要求是删一个还是全删。

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)

这里的排名定义为比当前数小的数的个数 +1

将树分裂为小于 val 的树 a 和大于等于 val 的树 b,那么小于 val 的树的个数即为 a 树的大小。

当排名的定义有变化时,方法也需要变化。

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)

前驱定义为小于 x,且最大的数,那么将树分裂为小于 x 的树 a 和 大于等于 x 的树 b,那么 x 的排名就是 a 的大小,于是可以用前面的 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)

后继定义为大于 x,且最小的数,那么与查询前驱类似,找到 x 的后继的排名,并调用 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 【模板】普通平衡树

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

  1. 插入一个数 x
  2. 删除一个数 x(若有多个相同的数,应只删除一个)。
  3. 定义排名为比当前数小的数的个数 +1。查询 x 的排名。
  4. 查询数据结构中排名为 x 的数。
  5. x 的前驱(前驱定义为小于 x,且最大的数)。
  6. x 的后继(后继定义为大于 x,且最小的数)。

对于操作 3,5,6,不保证当前数据结构中存在数 x

对于 100% 的数据,1n105|x|107

解法

这道题的操作就是上面分析的所有操作,整合一下即可。

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 不同。在这里,我们不按照权值 val 来分裂,而是按照子树大小 siz 分裂。具体来说,对于一个结点 cur,它的左子树的大小 +1 要为 cur 在原序列中的编号。

于是在分裂操作中,每个点按照左子树的大小分裂,由于 BST 性质,这样就可以保证 Treap 的中序遍历为原序列的顺序。

插入时,若插入结点在原序列中的编号为 i,按照 i 分裂,就可以得到一棵表示区间 [1,i] 的树和一棵表示区间 [i+1,n] 的树,将点合并进去即可。

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 【模板】文艺平衡树

您需要写一种数据结构(可参考题目标题),来维护一个有序数列。

其中需要提供以下操作:翻转一个区间,例如原有序序列是 5 4 3 2 1,翻转区间是 [2,4] 的话,结果是 5 2 3 4 1

对于 100% 的数据,1n,m1000001lrn

解法

这是一个序列问题,所以要用维护序列的平衡树。

对于区间翻转操作,由于树中每一个结点的中序遍历都是原序列中的一段区间,那么它的左右子节点也表示一段区间,并且合并后也是一段区间,所以只需要分裂出需要修改的区间,交换所有结点左右子节点即可。

但是这样复杂度显然不能接受,于是可以类似线段树,对结点打懒标记,这样复杂度即可接收。

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;
}

  1. https://oi.wiki/ds/bst ↩︎

  2. https://zh.wikipedia.org/wiki/平衡树# ↩︎

  3. https://zh.wikipedia.org/wiki/平衡树# ↩︎

本文作者:Luckies

本文链接:https://www.cnblogs.com/Luckies/p/18199309/treap

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Luckies  阅读(12)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起