平衡树 & 线段树
搜索树
满足左子树权值比自己小、右子树权值比自己大(感觉操作很像线段树/树状数组上二分)
rank(x)即 <x<x<x 的元素个数,按照根节点与 xxx 的大小关系递归到左右子树中去kth求第 kkk 小元素,对于每个节点维护子树大小,与求rank(x)类似
需要支持插入/删除元素
- 对于插入元素也是不断与根节点比较,递归到左右子树中,跑到叶子节点时构造一个新点,然后更新子树大小即可
- 如果有重复元素就记录一个 cntcntcnt 表示每个值的出现次数即可
如果树构成一条链那么就会退化成 O(n)O(n)O(n) 的操作复杂度。由此出现了平衡树。
平衡树
思路是不断改变树的形态使树的深度尽量小(达到 O(logn)O(\log n)O(logn))
Treap
每个点随机分配一个权值,期望时间复杂度 O(logn)O(\log n)O(logn)。Treap 上的每个点总是有有 (key,rand)(key,rand)(key,rand) 两个权值,后者为附加的随机值,则 Treap 上节点满足:
- 二叉搜索树的两个性质(keykeykey);
- 如果 vvv 是 uuu 的子节点,那么有 rand(u)>rand(v)rand(u)>rand(v)rand(u)>rand(v)。堆的性质,保证平衡性。
struct Treap {
int l,r,siz,v,rnd,w;
// 左儿子 右儿子 子树大小 关键字 随机值 出现次数
// 根据需要添加 tag、区间和等,和线段树类似
} tr[maxn];
void update(int k) {
tr[k].siz = tr[tr[k].l].siz + tr[tr[k].r].siz + tr[k].w;
}
维护序列时的 keykeykey 总是设为下标,优势在于可以插入/删除元素。
FHQ-Treap
基本操作只有分裂和合并,实现思路主要通过将需要操作的部分分裂出来,进行修改后再使用合并操作粘回去。每个操作都是 O(logn)O(\log n)O(logn) 的,实现简单故更为主流,对于整段区间的操作也很适合维护(如区间翻转、区间删除、区间插入)。
普通平衡树
维护以下操作:
- 插入一个数
- 删除一个数
- 查询 xxx 的排名
- 查询排名为 xxx 的数
- 求 xxx 前驱
- 求 xxx 后继
平衡树的直接应用,实现时总是以分裂 & 合并为主要思路。FHQ-Treap 记忆套路:对于实现中对左右子树不同的操作,总是截然不同甚至完全相反的。
-namespace FHQ_Treap {
struct Node {
int L, R;
int key, pri, siz;
} T[maxn << 1];
int ntot = 0, root = 0;
// 可以加入一个栈存储删除的节点,新创建节点时可以考虑使用之前空置的编号。
int Create(int x) {
T[++ ntot].siz = 1;
T[ntot].L = T[ntot].R = 0;
T[ntot].key = x, T[ntot].pri = rand();
return ntot;
}
void update(int rt) {
T[rt].siz = T[T[rt].L].siz + T[T[rt].R].siz + 1;
}
// l, r 表示分裂后的两颗子树的根节点。
// 类似于二分,由于平衡树满足搜索树的性质,故按照自己的权值与目标权值进行比较,从而决定向哪颗子树继续分裂。
void split(int rt,int val,int &l,int &r) { // 这是按照权值进行分裂。
if (rt == 0) return l = r = 0, void(0); // 边界情况,分裂到了叶节点之外。
if (T[rt].key <= val) { // 这一刀砍在哪里
l = rt; split(T[rt].R,val,T[rt].R,r);
} else {
r = rt; split(T[rt].L,val,l,T[rt].L);
} return update(rt);
} // 按照子树大小进行分裂是类似的,不过递归解决时需要更改目标的子树大小
int merge(int l,int r) {
if (l == 0 || r == 0) return l + r;
if (T[l].pri < T[r].pri) { // 按照优先级维护 FHQ-Treap 的平衡性
T[l].R = merge(T[l].R, r);
return update(l), l;
} else {
T[r].L = merge(l, T[r].L);
return update(r), r;
}
}
int insert(int val) {
int l, r; split(root, val, l, r); // 把 val 的地方分裂出来
int rt = Create(val);
root = merge(merge(l,rt),r); // 逐步合并
return rt;
}
int Delete(int val) {
int l, mid, r;
// 先得到包含待删除节点的根节点的子树,然后将子树中不属于待删除节点子树的节点进一步剥离
split(root, val, l, r); split(l, val - 1, l, mid);
mid = merge(T[mid].L, T[mid].R); // 此时 mid 就是待删除的节点,将 mid 两颗子树直接合并以湮灭 mid。
// 如果需要保存删除节点的空置位,那就将 mid 的编号递扔进栈里。
return root = merge(merge(l, mid), r); // 最后粘回去。
}
int findkth(int rt, int rnk) {
if (rnk == T[T[rt].L].siz + 1) // 如果找到了
return rt;
// 类似于线段树上二分,讨论左右子树的大小,判断目标节点的方向
if (rnk <= T[T[rt].L].siz)
return findkth(T[rt].L, rnk);
else return findkth(T[rt].R, rnk - T[T[rt].L].siz - 1);
}
}
区间操作
文艺平衡树
实现一颗支持区间翻转的平衡树。其中序列长度 n≤105n\le 10^5n≤105,操作次数 m≤105m\le 10^5m≤105。
区间反转
区间翻转就是平衡树专属的操作。我们将区间下标作为第一键值建树,那么对于 FHQ-Treap 上的每个节点,都可以代表一段区间信息。具体地,我们存储这个节点自己的信息、左子树的信息和右子树的信息。不同于线段树的是,Treap 上的每个点不仅代表区间,在计算这个点的贡献时自己的值也需要被考虑。
考虑把每次操作的区间分裂出来,类似于线段树打个旋转 tag。在分裂与合并等基本操作时应该及时下传旋转标记并进行旋转操作。
维护数列
给定一个数列,维护以下操作:
- 区间插入,即在指定位置插入一段区间
- 区间删除
- 区间修改
- 区间翻转
- 求区间和
- 询问整体最大子段和
区间翻转操作同上。
区间插入
对于待加入的数列,我们考虑将它们先建成一个 Treap,然后再将它粘到主 Treap 里去。类似于线段树的建树操作,每次分治操作先将 [L,mid][L,mid][L,mid] 和 [mid+1,R][mid+1,R][mid+1,R]中的点建好树,然后粘在一起。一开始对初始序列建树也可以这么做。
区间删除
由于我们将下标作为第一键值,所以对于一段连续的区间 [l,r][l,r][l,r] 一定可以被一个节点的子树表示出来,我们要做的就是把这个子树拆出来。将 FHQ-Treap 中前 l−1l-1l−1 个元素和后 n−rn-rn−r 个元素剥离出来,对于中间的元素我们递归地删除它们,并将它们的编号扔进栈中。
区间修改/求区间和/求最大子段和
这一类在线段树上好实现的操作可以几乎照搬到平衡树上来。不同的是,合并区间信息时需要考虑根节点的信息。剩下的就和线段树操作基本相同了。
Splay
每次操作都考虑把操作的点转到根上,但是旋转时要判断三点共线和折线两种情况进行旋转,这样操作复杂度就正确了(?noip 说不重要所以短时间内不打算学了。
替罪羊树
考虑左右孩子大小的比例,保持平衡的方法:定一个平衡因子 α\alphaα,操作与 bst 相同,但是插入时如果认为不平衡就拍扁重构。意思就是先中序遍历得到序列,然后用一个分治,每次把 midmidmid 作为根节点,左右区间递归下去作为左右子树。据说卡常专用。
文文的摄影布置
给出 nnn 个元素,第 iii 个元素有两个权值 AiA_iAi 和 BiB_iBi,令 f(i,j)f(i,j)f(i,j) 表示 Ai+Aj+mini<k<jBkA_i+A_j+\min_{i<k<j}B_kAi+Aj+mini<k<jBk,其中 i+1<ji+1<ji+1<j。共 mmm 次操作:
1 x y使 Ax←yA_x\gets yAx←y;2 x y使 Bx←yB_x\gets yBx←y;3 l r求maxl≤i<i+1<j≤rf(i,j) \max_{l\le i<i+1<j\le r}f(i,j) l≤i<i+1<j≤rmaxf(i,j)
分类讨论 (i,j,k)(i,j,k)(i,j,k) 三元组的位置,然后线段树维护需要的值即可。
火星人
维护一个字符串序列,支持以下操作:
- 单点插入;
- 单点修改;
- 查询两个区间的 LCP 即最长公共前缀的长度。
不管前面两个平衡树操作,考虑第三个询问。LCP 的一个做法即为二分答案,前缀哈希比对前面一段的长度。先考虑线段树,维护区间的前缀哈希,询问就做线段树上二分。最后把线段树操作改为平衡树即可支持插入操作。神奇的是这题直接用 STL 的 string 能过,均摊下来复杂度正确。
查找 Search
给定 nnn 个垃圾桶,你需要维护一个数据结构,支持以下操作:
1 pos val表示将 第 pospospos 个垃圾桶里的垃圾的编号换成 valvalval;
2 l r询问在 [l,r][l, r][l,r] 内是否存在垃圾编号和为 www 的 两个 垃圾桶。对于每个操作 2,若存在请输出
Yes,不存在请输出No。强制在线。
只考虑询问:记录前驱,即 pre(i)pre(i)pre(i) 表示 iii 左边最近的元素使得和为 www,预处理开一个桶从左往右扫一遍 O(n)O(n)O(n) 就能算完。考虑区间 [l,r][l,r][l,r],若对于所有的 l≤i≤rl\le i\le rl≤i≤r,pre(i)<lpre(i)<lpre(i)<l 那么就是无解的。于是记录 preprepre 的区间最大值即可。
现在考虑修改,仍然考虑 preprepre 数组。注意到对于序列 {1,2,2}\{1,2,2\}{1,2,2},最后一个 222 对答案的贡献被离 111 最近的那个 222 覆盖了。于是我们每次只用修改最左边的 preprepre 即可。将 xxx 替换为 yyy 相当于删去 xxx 然后原地插入 yyy。对于删除,我们要找到分别找到左右第 111 个的 xxx 和 w−xw-xw−x,即支持找到 iii 位置右边第一个 =x=x=x 的值。考虑对于每个值用一个平衡树维护它的下标,那么查询就到对应的平衡树中去。实现时完全可以用 set 的 lower_bound 实现。

701






