跟 BST 爆了!
背景:2024/8/7 的正睿 ab 班省选模拟赛第二场中,t1 由于不会平衡树而上网疯狂找板子、卡空间导致浪费 3.5h,警钟长鸣。
随机情况下表现良好的 BST
动态开点建立 BST。支持查询排名、前驱、后继等操作。
代码。
时间复杂度最坏 \(O(n^2)\),随机情况下 \(O(n\log n)\),因为树高期望为 \(O(\log n)\)。
treap
平衡树存在性质:中序遍历是有序的。
考虑项笛卡尔树一样构建一棵平衡树,从有序序列中选择一个数 \(x\),对左边右边进行分治构建。不难发现,无论 \(x\) 是什么,都可以构建出一棵正确的 BST,其中选择中位数时形态最良好——这也是替罪羊树的思想。
对每个数赋优先级 \(r_i\),每次选择 \(r_i\) 最大的作为根,进行分治构建。不难发现,此时的结构中 \(val\) 满足 BST 的性质,\(r_i\) 满足堆的性质,因此成为 treap。
随机划分优先级使得期望的树高为 \(O(\log n)\),并且树上两点之间的期望距离为 \(O(\log d(x,y))\),其中 \(d(x,y)\) 为序列中两数的距离。
treap 的空间复杂度为 \(O(n)\)。
treap 上的操作主要通过两种方式实现:
split
按值/排名对 split 进行分裂,即对于阈值 \(T\),分裂出两棵形态良好的 BST。
注意到如果能分裂出两棵 BST,则 BST 的形态一定良好,这取决于分裂前原树形态的良好。
假设按照值对阈值 \(T\) 进行分裂。
存在两个分治构建中的参数 \(rt_1\) 和 \(rt_2\),代表构建过程中两棵树的根指针。设当前考虑的点为 \(p\),对 \(val_p\) 和 \(T\) 的大小关系进行讨论:
- 若 \(val_p\le T\)。
将 \(p\) 及其左子树扔到 \(rt_1\),对其右子树进行分治构建,更新指针:\(rt_1\leftarrow rs_{rt_1}\)。
- 若 \(val_p>T\)。
将 \(p\) 的右子树扔到 \(rt_2\),对其左子树进行分治构建,更新指针:\(rt_2\leftarrow ls_{rt_2}\)。
不难发现,split 操作的复杂度为 \(O(h)=O(\log n)\)。
merge
将两棵 treap 合并成一棵 treap。
merge 也有一个隐性的阈值 \(T\),满足两棵树符合对于 \(T\) 进行 split 后的结果。
由于两棵树都已经满足了 treap 的性质,每次只需要比较根节点的 \(r_i\)(随机优先级),考虑把谁变成根节点即可。
- 若左树的 \(r_i\) 更大。
将左树的根节点作为整棵树的根节点,将左树的右子树和右树进行合并。
- 若右树的 \(r_i\) 更大。
将右树的根节点作为整棵树的根节点,将右树的左子树和左树进行合并。
不难发现,merge 操作的复杂度为 \(O(h)\)。这里 \(O(h)\) 同样趋近于 \(O(\log n)\)。
证明:treap 形态唯一。
这个结论的前提是,\(r_i\) 互不相同。
考虑对于序列构建 treap 的过程,选出 \(r_i\) 最大的节点作为根,对小于 \(val_i\) 的点和大于等于 \(val_i\) 分治构建。
当 \(r_i\) 唯一时,可以发现每次选出的点都是唯一的,因此 treap 形态唯一。
其实当 \(r_i\) 不相同时,在随机选取的情况下,所有 treap 的形态虽然不同,但树高都是 \(O(\log n)\) 级别。
由于 treap 形态唯一,所以 \(O(h)\) 趋近于有序序列构建时的树高 \(O(\log n)\)。
用 merge 和 split 实现基本操作
插入
设插入的数为 \(x\),先按照 \(x\) 对 treap 进行 split,再按照 \(x+1\) 对右树 split。
此时能判断原树是否存在 \(val=x\) 的点(如果有,一定已经被 split 成了一棵单独的树),如果有,直接插入。
否则将 \(x\) 新建一棵 treap 进行合并。
注意 merge 时的参数顺序问题。
删除
与插入一样,找到 \(val=x\) 的节点进行删除。
查询某个值的排名
直接 split 后查询。
查询某个排名的值
可以直接在原树上 \(O(h)\) 查询。
有一种 split_by_rank 的操作,阈值 \(T\) 指的是一个排名,会 split 出三棵 treap,分别为 \(<T\),\(=T\),\(>T\)。
前驱后继
可以直接在原树上查,也可以 split 后找最值。
\(r_i\) 的作用
是为了使 merge 操作更加随机,因此初始一棵树高正确但不满足 heap 性质的 BST 也是可以的。
区间操作
单点插入,单点查询
背景的来源。
利用三分做到 \(O(n\log_3 n)\) 的询问次数是简单的,唯一需要考虑的就是如何在某个数后插入一个数和查询某个位置的值,强制在线。
将 \(p\) 作为关键字建立 treap,同时在节点上维护 \(x\),插入时 split 出两棵 treap,给其中的一棵 treap 打上 tag 后 merge 即可。
查询时注意下放 tag。
此做法期望得分 \(55\) 分,这也是场上大部分人的分数,但是他们的实现为整体二分。常数小一点可以获得更高的分数。