世界杯亚洲球队

数据结构——平衡树

数据结构——平衡树

GJX_Algorithm

·

2025-03-01 19:08:28

·

算法·理论

平衡树

二叉搜索树 & 平衡树

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

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

每个节点的左子树所有节点的值小于当前节点的值。

每个节点的右子树所有节点的值大于当前节点的值。

左右子树也分别为二叉搜索树。

时间复杂度分析:

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

最坏情况(高度最大):二叉搜索树退化成链表时,若总节点数为 n,则树的高度为 n。其插入、删除、查询操作的时间复杂度皆为 O(n)。

平衡树(Balance Tree)基础

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

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

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

平衡树类型

平衡策略

Treap

随机优先级,左旋与右旋

Splay

贪心

FHQ-Treap(无旋 Treap)

无旋,分裂+合并

FHQ-Treap 的实现

为什么要选择 FHQ-Treap?

无旋操作:目前笔者只学过 FHQ-Treap,似乎分裂与合并比左旋右旋更容易理解?

天然支持区间操作:结合延迟标记(如区间翻转),之后文艺平衡树会讲到。

高效稳定:所有操作严格 O(\log n)。

数据结构的定义与初始化:

struct node

{

int val, rnk, lc, rc, size; // 值、优先级、左右子节点、子树的大小

}tree[N];

int 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(); //Treap 的基本策略,随机优先级

return tot; //返还对应的结点编号

}

功能:

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

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

分裂

主要思路如图:

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 ;

}

功能:给定一棵平衡树 k,按照某个权值 val,分裂成平衡树 a 和 b,且 a 树所有点的权值不超过 val,b 树的所有点权大于 val。

合并

主要思路如图:

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 ;

}

功能:将两棵平衡树 a 和 b 合并成一棵平衡树 k,且保证 a 树的点权都小于 b 树的点权。

插入

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 的节点,处理重复值情况。

查询

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); // 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)。

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

文艺平衡树

什么是文艺平衡树?

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

区间翻转:将指定区间内的元素逆序。

区间求和/最值:快速查询区间内的统计值。

区间赋值:批量修改区间内的元素。

核心需求:在维持平衡树性质的同时,高效处理区间操作,时间复杂度需保持在 O(\log n)。

FHQ-Treap 的文艺平衡树实现

懒惰标记:说起区间操作,相比读者会想起经典的线段树,那线段树在某些方面的核心操作就是懒惰标记(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 ;

}

区间操作

分裂提取区间:通过两次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

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;

}

P3391 【模板】文艺平衡树

#include

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;

}

练习

P1486 [NOI2004] 郁闷的出纳员

P2234 [HNOI2002] 营业额统计

P3224 [HNOI2012] 永无乡

P3850 [TJOI2007] 书架

P3765 总统选举

P5217 贫穷