平衡树
平衡树
全称是二叉搜索平衡树
二叉搜索树:对于一棵二叉树,每个节点有权值 ,任意节点 的左子树的权值小于等于 ,右子树反之。
二叉搜索树的优点:
- 具有单调性,可以 的时间查找。
- 二叉搜索树的中序遍历是单调不减的,可以将下标当做权值,这样一个序列可以映射到一棵二叉搜索树。
为什么需要平衡树?
- 一个序列对应的二叉搜索树不唯一。我们希望找到高度最小的那颗应用。
若一棵二叉树的任意一个结点 ,其左右子树高度差不超过 ,称为二叉平衡树。
算法竞赛领域常用的平衡树:
- treap
- splay
- fhq_treap
其中,treap 和 fhq_treap 利用的是随机平衡,不追求绝对平衡。splay 是贪心策略平衡。treap 和 splay 都是通过旋转改变形态,fhq_treap 通过分裂和合并来改变形态。
Fhq_treap 实现
-
分裂操作
将一棵平衡树分裂成 和 两棵子树,且 。 是选取的某个数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28struct node {
int val, l, r, size, rnk;
}tree[N];
void update(int cur)
{
tree[cur].size = tree[tree[cur].l].size + tree[tree[cur].r].size + 1;
return;
}
void split(int cur, int& a, int& b, int val)
{
if (!cur)
{
a = b = 0;
return;
}
if (tree[cur].val <= val)
{
a = cur;
split(tree[cur].r, tree[cur].r, b, val);
}
else
{
b = cur;
split(tree[cur].l, a, tree[cur].l, val);
}
update(cur);
return;
} -
合并操作
将 两棵平衡树合并,且 平衡树的任意权值一定小于等于 平衡树的任意权值。
模板完整代码:
1 |
|
例题
洛谷P1486
NOI2004 的题。
第一行有两个整数 和 。 表示下面有多少条命令, 表示工资下界。
接下来的 行,每行一个字符 和一个整数 ,表示一条命令。命令可以是以下四种之一:
I k
新建一个工资档案,初始工资为 。如果某员工的初始工资低于工资下界,他将立刻离开公司。A k
把每位员工的工资加上 。S k
把每位员工的工资扣除 。F k
查询第 多的工资。在初始时,可以认为公司里一个员工也没有。
维护一个全局懒标记,懒标记减的时候删除离开的人。
1 | signed main() |
洛谷P2234
而分析整个公司的从成立到现在营业情况是否稳定,只需要把每一天的最小波动值加起来就可以了。你的任务就是编写一个程序帮助 Tiger 来计算这一个值。
我们定义,一天的最小波动值 = 。
特别地,第一天的最小波动值为第一天的营业额。
和前驱后继减,即可保证最小。
1 | signed main() |
洛谷P3224
永无乡包含 座岛,编号从 到 ,每座岛都有自己的独一无二的重要度,按照重要度可以将这 座岛排名,名次用 到 来表示。某些岛之间由巨大的桥连接,通过桥可以从一个岛到达另一个岛。如果从岛 出发经过若干座(含 座)桥可以 到达岛 ,则称岛 和岛 是连通的。
现在有两种操作:
B x y
表示在岛 与岛 之间修建一座新桥。
Q x k
表示询问当前与岛 连通的所有岛中第 重要的是哪座岛,即所有与岛 连通的岛中重要度排名第 小的岛是哪座,请你输出那个岛的编号。
使用并查集+平衡树,合并使用启发式合并,即拆小的往大的上 merge。
1 | int find(int x) |
洛谷P6136
1 | signed main() |
文艺平衡树(fhq_treap 版)
需要区间反转时,为了实现区间翻转,我们使用懒标记(lazy tag),像线段树一样。
-
pushdown 函数
1
2
3
4
5
6
7
8
9
10void pushdown(int cur)
{
if (!tree[cur].tag)
return;
swap(tree[cur].l, tree[cur].r);
tree[tree[cur].l].tag ^= 1;
tree[tree[cur].r].tag ^= 1;
tree[cur].tag = 0;
return;
} -
split 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void split(int cur, int& a, int& b, int val)
{
if (!cur)
{
a = b = 0;
return;
}
pushdown(cur);
if (tree[tree[cur].l].size + 1 <= val)//文艺平衡树是按size分的
{
a = cur;
split(tree[cur].r, tree[cur].r, b, val - tree[tree[cur].l].size - 1);
}
else
{
b = cur;
split(tree[cur].l, a, tree[cur].l, val);
}
update(cur);
return;
} -
merge 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22void merge(int& cur, int a, int b)
{
if (!a || !b)
{
cur = a + b;
return;
}
if (tree[a].rnk <= tree[b].rnk)
{
pushdown(a);
cur = a;
merge(tree[a].r, tree[a].r, b);
}
else
{
pushdown(b);
cur = b;
merge(tree[b].l, a, tree[b].l);
}
update(cur);//记得update,forget to update 3 times
return;
} -
reverse 函数
1
2
3
4
5
6
7
8
9
10void reverse(int& cur, int x, int y)
{
int a = 0, b = 0, c = 0;
split(cur, a, b, y);
split(a, a, c, x - 1);
tree[c].tag ^= 1;
merge(a, a, c);
merge(cur, a, b);
return;
} -
print 函数
1
2
3
4
5
6
7
8
9
10void print(int x)
{
if (!x)
return;
pushdown(x);
print(tree[x].l);
cout << tree[x].val << ' ';
print(tree[x].r);
return;
}
例题
洛谷P3391
洛谷P3850
Knuth 先生家里有个精致的书架,书架上有 N 本书,如今他想学到更多的知识,于是又买来了 M 本不同的新书。现在他要把新买的书依次插入到书架中,他已经把每本书要插入的位置标记好了,并且相应的将它们放好。由于 Knuth 年龄已大,过几天他已经记不清某些位置上放的到底是什么书了,请问你能帮助他吗?
文艺平衡树加一个 string
就可以了
1 | struct node { |