首页 > 其他分享 >浅谈平衡树

浅谈平衡树

时间:2024-07-25 12:57:46浏览次数:8  
标签:return 浅谈 val int tree Treap 平衡 cur

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

二叉搜索树(BST)

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

  1. 空树是二叉搜索树。

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

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

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

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

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

平衡树

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

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

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

Treap

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

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

下文称随机附加值为 \(rnk\)。

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

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

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

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

无旋 Treap

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

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

分裂(Split)

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

FHQ-Treap(1)

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

至于具体的分裂过程,同样依靠二叉搜索树的性质,进行递归分裂。对于当前搜到的结点 \(cur\),设它的权值为 \(val\),若 \(cur \le key\),根据二叉搜索树的性质,左子树的结点权值均小于根节点,那么 \(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 合并。设它们的根分别为 \(a\) 和 \(b\),那么 \(a\) 中所有点的权值必须小于等于 \(b\) 中的权值。在合并操作中,我们通过维护 \(rnk\) 的值来维护合并后 Treap 的平衡,不再维护 \(val\),因此为了使合并后的 Treap 是 BST,\(a\) 中的权值必须小于等于 \(b\) 中的权值。

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

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\),所以此时 \(a\) 与 \(c\) 可以合并。

将 \(a\) 与 \(c\) 合并,设根结点仍然为 \(a\)。此时 \(a\) 中点权仍然小于等于 \(val\),所以仍然可以与 \(b\) 合并。\(a\) 与 \(b\) 合并后的 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\%\) 的数据,\(1\le n \le 10^5\),\(|x| \le 10^7\)。

解法

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

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\%\) 的数据,$1 \le n, m \leq 100000 $,\(1 \le l \le r \le n\)。

解法

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

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

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

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/平衡树# ↩︎

标签:return,浅谈,val,int,tree,Treap,平衡,cur
From: https://www.cnblogs.com/Luckies/p/18199309/treap

相关文章

  • 基于完美反射假设的自动白平衡算法
    简介完美反射假设(PerfectReflectorAssumption)是另一种常见的自动白平衡(AWB)方法。它假设图像中有一些物体具有完美反射特性,能够反射所有入射光。这意味着这些物体的反射光应该是白色或灰色,通过识别这些物体并调整图像的色彩平衡,可以实现白平衡。原理完美反射假设的核心思想......
  • 基于平衡优化器算法优化的TSP问题求解
    智能优化算法应用:基于平衡优化器算法的TSP问题求解-附代码文章目录智能优化算法应用:基于平衡优化器算法的TSP问题求解-附代码1.TSP问题3.平衡优化器算法4.实验参数设定5.算法结果6.Matlab代码7.Python代码摘要:TSP是数学领域内一道著名的难题之一,如何求解一直是......
  • 在设计电子产品时,如何平衡抗震和抗振需求以确保产品的整体性能?
            在电子产品的设计中,抗震和抗振需求是两个重要的考量因素,它们虽然在目标上有所重叠,即都是为了确保产品在各种使用环境下的可靠性和稳定性,但它们的侧重点和应对措施有所不同:抗震需求:定义:抗震需求通常指的是电子产品在设计时需要考虑到的抵抗外部冲击和震动的......
  • 浅谈5G频段的选择问题
    在5G频段选择的问题上,我们需要考虑多个方面,包括频段特性、网络需求、设备支持以及运营商的部署情况等。以下是对5G频段选择问题的详细分析:一、频段特性    低频段(如700MHz,即N28/N28a):        优势:通信距离远,穿透能力强,适合广覆盖和深度覆盖,尤其适合人口密集的城市区......
  • 如何在 PyTorch 中使用类权重和焦点损失来处理多类分类的不平衡数据集
    我正在研究语言任务的多类分类(4类),并使用BERT模型进行分类任务。我正在关注这篇博文NLP的迁移学习:微调BERT用于文本分类我的BERT微调模型返回nn.LogSoftmax(dim=1)我的数据非常不平衡,所以我使用了|||计算类别的权重并使用损失中的权重。......
  • 如何优雅地写注释:找到代码注释的黄金平衡点
    在软件开发的世界里,注释是代码的伴侣,它们帮助我们记录思路,解释复杂的逻辑,以及为后来者提供指引。然而,注释的艺术在于找到恰当的平衡——既不过于冗余,也不过于吝啬。本文将探讨如何优雅地写出恰到好处的注释。注释有啥用首先,我们需要认识到注释的价值。好的注释可以:提高代码的......
  • 如何平衡 PyTorch 数据集?
    我有一个不平衡的PyTorch数据集。A和V样本的数量远低于其他样本我想平衡我的数据集,即使我必须删除属于主流类的样本。怎么办?现在,如果某些类的数量超过某个固定值,我只需删除某些类的样本。这在技术上比较复杂并且不方便。也许有一些sklearn或PyTorch方法可......
  • cs04 浅谈编译和链接
    C/C++语言中编译和链接通常都是自动完成的,win上VS全部包圆了,什么都不用操心,linux上使用cmake编写CMakeLists.txt也可以使用短短几行代码构建一个工程。那么编译和链接到底在我们看不到的地方做了什么呢?深入理解计算机系统中有一句话大多数编译系统提供了编译器驱动程序(com......
  • 浅谈Mike 3D中制作垂向水温分层边界
    前言:给大家更新一篇全新模块的讲解吧,MIKE3D中难点在于制作一种分层DFS2的边界,这种边界必须用实测的数据作为支撑,本次讲解以水温分层为例。step1:打开GridSeries中BlankGird,新建2dgridstep2:进入参数设置界面,坐标与模型设置一致,坐标原点可以设置成边界起点坐标(左手......
  • 代码随想录算法训练营第十五天 | 110.平衡二叉树、257. 二叉树的所有路径 、404.左叶
    110.平衡二叉树题目:.-力扣(LeetCode)思路:后序遍历计算高度代码:/***Definitionforabinarytreenode.*structTreeNode{*intval;*TreeNode*left;*TreeNode*right;*TreeNode():val(0),left(nullptr),right(nullptr){}*......