数据结构——平衡树

· · 算法·理论

平衡树

二叉搜索树 & 平衡树

二叉搜索树(Binary Search Tree)基础

  1. 定义:二叉搜索树是一种二叉树结构,满足如下性质:

    • 每个节点的左子树所有节点的值小于当前节点的值。
    • 每个节点的右子树所有节点的值大于当前节点的值。
    • 左右子树也分别为二叉搜索树。
  2. 时间复杂度分析:

    • 理想情况(高度最小):二叉搜索树为完全平衡树时,若总节点数为 n,则树的高度为 \lfloor \log_2n \rfloor。其插入、删除、查询操作的时间复杂度皆为 O(\log n)
    • 最坏情况(高度最大):二叉搜索树退化成链表时,若总节点数为 n,则树的高度为 n。其插入、删除、查询操作的时间复杂度皆为 O(n)

平衡树(Balance Tree)基础

  1. 广泛定义:是指一棵树形结构,对任意节点 x,其左右子树的高度差不超过 1。

  2. 二叉搜索平衡树(后文提到的平衡树皆指二叉搜索平衡树):上文提到 BST 的退化问题,为了解决,利用平衡树特定的平衡性质,使二叉搜索树的高度始终保持理想情况。以下给出普通 BST(退化的 BST)与经平衡后的 BST 的区别:

  1. 算法竞赛中常见的平衡树实现,本文重点介绍 FHQ-Treap:

    平衡树类型 平衡策略
    Treap 随机优先级,左旋与右旋
    Splay 贪心
    FHQ-Treap(无旋 Treap) 无旋,分裂+合并

FHQ-Treap 的实现

  1. 为什么要选择 FHQ-Treap?

    • 无旋操作:目前笔者只学过 FHQ-Treap,似乎分裂与合并比左旋右旋更容易理解?
    • 天然支持区间操作:结合延迟标记(如区间翻转),之后文艺平衡树会讲到。
    • 高效稳定:所有操作严格 O(\log n)
  2. 数据结构的定义与初始化:

    struct node
    {
      int val, rnk, lc, rc, size; // 值、优先级、左右子节点、子树的大小
    }tree[N];
    int tot, root; // 总节点数与根节点
    • 功能:储存树的信息。
  3. 结点的管理

    void update(int k)
    {
      tree[k].size = tree[tree[k].lc].size + tree[tree[k].rc].size + 1;
      return ;
    }
    int add_node(int val) 
    {
      tree[++tot].size = 1;
      tree[tot].val = val;
      tree[tot].lc = tree[tot].rc = 0;
      tree[tot].rnk = rand(); //Treap 的基本策略,随机优先级
      return tot; //返还对应的结点编号
    }
    • 功能:

    • update:统计子树大小,左儿子子树大小+右儿子子树大小+自己。

    • add_node:新建节点并初始化结点信息。

  4. 分裂

    • 主要思路如图:

  void split(int k, int &a, int &b, int val) 
  {
      if(k == 0) // 结点不存在
    {
          a = b = 0;
          return ;
      }
      if(tree[k].val <= val) // 按值分裂,以 k 为根的子树
    {
          a = k;
          split(tree[k].rc, tree[k].rc, b, val); // 继续分裂以 k 的右儿子为根的子树,分成 k 的右儿子本身与 b 树
      } 
    else 
    {
          b = k;
          split(tree[k].lc, a, tree[k].lc, val);
      }
      update(k);
      return ;
  }
  1. 合并
    • 主要思路如图:

  void merge(int &k, int a, int b)
  {
      if(a == 0 || b == 0) 
    {
          k = a + b;
          return ;
      }
      if(tree[a].rnk < tree[b].rnk) // a 的优先级更高,a 做根结点,且 a 的右孩子与 b 竞争成为 a 的右孩子
    {
          k = a;
          merge(tree[a].rc, tree[a].rc, b);
      } 
    else 
    {
          k = b;
          merge(tree[b].lc, a, tree[b].lc);
      }
      update(k);
      return ;
  }
  1. 插入

    void insert(int &k, int val) 
    {
      int a = 0, b = 0, cur = add_node(val); 
      split(k, a, b, val);
      merge(a, a, cur);
      merge(k, a, b);
      return ;
    }
    void del(int &k, int val) 
    {
      int a = 0, b = 0, z = 0;
      split(k, a, b, val); // 先把比 val 大的子树划出去
      split(a, a, z, val - 1); // 把值为 val 的节点分裂出来,成为 z 树
      merge(z, tree[z].lc, tree[z].rc); // 将待删除的 z 节点的左右子树合并,取代 z 的位置
      merge(a, a, z);
      merge(k, a, b);
      return ;
    }
    • 功能:

    • insert:插入新值 val,通过分裂找到位置后合并。

    • del:删除值为 val 的节点,处理重复值情况。

  2. 查询

    int find_num(int k, int x) 
    {
      while(tree[tree[k].lc].size + 1 != x) 
    {
          if(tree[tree[k].lc].size >= x)
              k = tree[k].lc;
          else 
        {
              x -= tree[tree[k].lc].size + 1;
              k = tree[k].rc;
          }
      }
      return tree[k].val;
    }
    int find_rank(int &k, int val) 
    {
      int a = 0, b = 0;
      split(k, a, b, val - 1); // 分裂出 <val 的节点
      int tmp = tree[a].size + 1;
      merge(k, a, b);
      return tmp;
    }
    int prev(int &k, int val) 
    {
      int a = 0, b = 0;
      split(k, a, b, val - 1);
      int tmp = find_num(a, tree[a].size); // a树的最大值即前驱
      merge(k, a, b);
      return tmp;
    }
    int suf(int &k, int val)
    {
      int a = 0, b = 0;
      split(k, a, b, val);
      int tmp = find_num(b, 1); // b树的最小值即后继
      merge(k, a, b);
      return tmp;
    }
    • 功能:

    • find_num:通过遍历树结构找到第 k 小的元素。

    • find_rank:查询值 val 的排名(比 val 小的元素数量 + 1)。

    • prevsuf:分别通过分裂树找到前驱(比 val 小的最大元素)和后继(比 val 大的最小元素)。

文艺平衡树

什么是文艺平衡树?

  1. 定义:文艺平衡树泛指支持区间操作的平衡树,例如:

    • 区间翻转:将指定区间内的元素逆序。
    • 区间求和/最值:快速查询区间内的统计值。
    • 区间赋值:批量修改区间内的元素。
  2. 核心需求:在维持平衡树性质的同时,高效处理区间操作,时间复杂度需保持在 O(\log n)

FHQ-Treap 的文艺平衡树实现

  1. 懒惰标记:说起区间操作,相比读者会想起经典的线段树,那线段树在某些方面的核心操作就是懒惰标记(Lazy Tag)。

    • 原理:将区间操作的修改暂时记录在结点标记中,仅在必要时(如查询或进一步分裂)下传给子结点。
    • 示例:区间翻转。
      void pushdown(int k) 
      {
      if (tree[k].rev) 
      {
          swap(tree[k].lc, tree[k].rc); // 交换左右子节点
          tree[tree[k].lc].rev ^= 1; // 下传标记到左子树
          tree[tree[k].rc].rev ^= 1; // 下传标记到右子树
          tree[k].rev = 0; // 清除当前节点的标记
      }
      return ;
      }
  2. 区间操作

    • 分裂提取区间:通过两次split分离出目标区间。
    • 应用标记:在区间根节点上设置标记(如 rev ^= 1)。
    • 合并还原树:将处理后的子树合并回原树。
    • 示例:区间翻转。
      void reverse(int l, int r) 
      {
      int a, b, c;
      // 分割出前 l - 1 个节点 a 和剩余部分 b + c
      split(root, a, b, l - 1);
      // 从剩余部分分割出 r - l + 1 个节点 b 和剩余部分 c
      split(b, b, c, r - l + 1);
      tree[b].rev ^= 1; // 标记翻转
      merge(a, a, b); // 合并 a 和翻转后的 b
      merge(root, a, c); // 合并得到最终树
      return ;
      }

      完整代码示例

    • P3369 【模板】普通平衡树
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
const int INF = 0x3f3f3f3f;
struct node 
{
    int val, rnk, lc, rc, size;
}tree[N];
int n, tot, root;
void update(int k)
{
    tree[k].size = tree[tree[k].lc].size + tree[tree[k].rc].size + 1;
    return ;
}
int add_node(int val) 
{
    tree[++tot].size = 1;
    tree[tot].val = val;
    tree[tot].lc = tree[tot].rc = 0;
    tree[tot].rnk = rand();
    return tot;
}
void split(int k, int &a, int &b, int val) 
{
    if (k == 0) 
    {
        a = b = 0; 
        return ;
    }
    if (tree[k].val <= val)
    {
        a = k;
        split(tree[k].rc, tree[k].rc, b, val);
    } 
    else 
    {
        b = k;
        split(tree[k].lc, a, tree[k].lc, val);
    }
    update(k);
    return ;
}
void merge(int &k, int a, int b) 
{
    if (a == 0 || b == 0) 
    {
        k = a + b;
        return ;
    }
    if (tree[a].rnk < tree[b].rnk) 
    {
        k = a;
        merge(tree[a].rc, tree[a].rc, b);
    } 
    else 
    {
        k = b;
        merge(tree[b].lc, a, tree[b].lc);
    }
    update(k);
    return ;
}
void insert(int &k, int val) 
{
    int a = 0, b = 0, cur = add_node(val); 
    split(k, a, b, val);
    merge(a, a, cur); 
    merge(k, a, b);
    return ;
}
void del(int &k, int val) 
{
    int a = 0, b = 0, z = 0;
    split(k, a, b, val);
    split(a, a, z, val - 1); 
    merge(z, tree[z].lc, tree[z].rc);
    merge(a, a, z);
    merge(k, a, b);
    return ;
}
int find_num(int k, int x) 
{
    while (tree[tree[k].lc].size + 1 != x) 
    {
        if (tree[tree[k].lc].size >= x)
            k = tree[k].lc;
        else 
        {
            x -= tree[tree[k].lc].size + 1;
            k = tree[k].rc;
        }
    }
    return tree[k].val;
}
int find_rank(int &k, int val) 
{
    int a = 0, b = 0;
    split(k, a, b, val - 1);
    int tmp = tree[a].size + 1;
    merge(k, a, b);
    return tmp;
}
int prev(int &k, int val) 
{
    int a = 0, b = 0;
    split(k, a, b, val - 1);
    int tmp = find_num(a, tree[a].size);
    merge(k, a, b);
    return tmp;
}
int suf(int &k, int val)
{
    int a = 0, b = 0;
    split(k, a, b, val);
    int tmp = find_num(b, 1);
    merge(k, a, b);
    return tmp;
}
int main() 
{
    cin >> n;
    root = 0;
    srand(time(0));
    for (int i = 1; i <= n; i++) 
    {
        int op, val;
        cin >> op >> val;
        if (op == 1) 
            insert(root, val);
        else if (op == 2) 
            del(root, val);
        else if (op == 3) 
            cout << find_rank(root, val) << "\n";
        else if (op == 4) 
            cout << find_num(root, val) << "\n";
        else if (op == 5) 
            cout << prev(root, val) << "\n";
        else 
            cout << suf(root, val) << "\n";
    }
    return 0;
}
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
struct node 
{
    int val, rnk, lc, rc, size;
    bool rev;
}tree[N];
int n, m, root, tot;
void update(int k) 
{
    tree[k].size = tree[tree[k].lc].size + tree[tree[k].rc].size + 1;
    return ;
}
void pushdown(int k) 
{
    if (tree[k].rev) 
    {
        swap(tree[k].lc, tree[k].rc);
        tree[tree[k].lc].rev ^= 1;
        tree[tree[k].rc].rev ^= 1;
        tree[k].rev = 0;
    }
    return ;
}
int add_node(int val) 
{
    tree[++tot].size = 1;
    tree[tot].val = val;
    tree[tot].lc = tree[tot].rc = 0;
    tree[tot].rnk = rand();
    tree[tot].rev = 0;
    return tot;
}

void split(int k, int &a, int &b, int sz) 
{
    if (k == 0) 
    {
        a = b = 0;
        return;
    }
    pushdown(k);
    if (tree[tree[k].lc].size + 1 <= sz) 
    {
        a = k;
        split(tree[k].rc, tree[k].rc, b, sz - tree[tree[k].lc].size - 1);
    } 
    else 
    {
        b = k;
        split(tree[k].lc, a, tree[k].lc, sz);
    }
    update(k);
    return ;
}
void merge(int &k, int a, int b) 
{
    if (a == 0 || b == 0) 
    {
        k = a + b;
        return;
    }
    if (tree[a].rnk < tree[b].rnk) 
    {
        pushdown(a);
        k = a;
        merge(tree[a].rc, tree[a].rc, b);
    } 
    else 
    {
        pushdown(b);
        k = b;
        merge(tree[b].lc, a, tree[b].lc);
    }
    update(k);
    return ;
}
void insert(int &k, int val) 
{
    int a = 0, b = 0, cur = add_node(val); 
    split(k, a, b, val);
    merge(a, a, cur); 
    merge(k, a, b);
    return ;
}
void reverse(int l, int r) 
{
    int a = 0, b = 0, c = 0;
    split(root, a, b, l - 1);
    split(b, b, c, r - l + 1);
    tree[b].rev ^= 1;
    merge(a, a, b);
    merge(root, a, c);
    return ;
}
void print(int k) 
{
    if (k == 0) return;
    pushdown(k);
    print(tree[k].lc);
    cout << tree[k].val << " ";
    print(tree[k].rc);
    return ;
}
int main() 
{
    cin >> n >> m;
    root = 0;
    srand(time(0));
    for (int i = 1; i <= n; i++)
        insert(root, i);
    for (int i = 1; i <= m; i++) 
    {
        int l, r;
        cin >> l >> r;
        reverse(l, r);
    }
    print(root);
    return 0;
}

练习