前言
噫,好!我又来了
这几天闲着没事翻桃片,发现好多人卡评测被封了,就看了看紫荆花,发现 splay 会被卡,就去学了替罪羊树($\text{Scapegoat Tree}$)。
正文
写在前面
大家都知道,splay 是通过旋转维护整棵树的平衡,但也会出现深度过大而导致复杂度爆炸的情况。
而替罪羊树则是通过重构来维护平衡,他会初始一个失衡系数 $\alpha$,一般取 $[0.7,0.8]$,然后通过比较左右子树大小和自身大小来判断是否失衡,若失衡,就拍扁重构
然后是关于删除的一个问题,替罪羊树用的是惰性删除(把当前节点个数清零,但不从树中删去),记住这个东西,对之后的很多东西是都有影响的。
关于结构体
1 | struct Node { |
可以发现,基本上与 splay 没有什么太大区别,但多了,三个 siz 并且没有 fa(因为他不需要遍历父亲)。
解读一下这三个 siz。
sizt:子树中有多少个不同的权值(包括空节点)sizv:子树中有多少个节点(不包括空节点)siznd:子树中有多少个不同的权值(不包括空节点)
对应的 upd 应该这么写
1 | void upd(int x) { |
关于重构
就是把一棵树展开进一个线性表中,再二分建树(空节点不建树),比较简单,直接上代码了。
1 | void rbuflatten(int x) { |
判断是否重构有三个条件
本节点是否为空
自己的
sizt乘上 $\alpha$ 是否小于左右子树中最大的子树的sizt自己的
sizt乘上 $\alpha$ 是否大于自己的siznd
关于第三条,因为惰性删除,删除操作一旦过多,会导致树中的空节点过多,显而易见,空节点多没有用,所以,可通过重构操作删去。
1 | bool ifrbu(int x) { return (t[x].num and ((t[x].sizt*A<=(double)std::max(t[t[x].ch[0]].sizt,t[t[x].ch[1]].sizt)) or (t[x].sizt*A>=(double)t[x].siznd))); } |
查询排名
替罪羊树有两个函数,一个为 rkf,是查询比 $k$ 小的数中最大数的排名,一个是 rkl,是查询比 $k$ 大的数中最小数的排名。
两个函数写法基本相同,$k$ 比本节点大往右子树搜,比本节点小往左子树搜。
但是!替罪羊树采用惰性删除,所以显而易见的,当 $k$ 等于本节点时如果 num 不等于 $0$ 是可以返回的,但如果等于 $0$ 呢?
两个函数有两个不同的写法。
对于 rkf 因为他是要找比 $k$ 小的数中的最大数的排名,所以他的返回值为本节点的左子树的 sizv,那如果 $k$ 所代表的节点的 num 为 $0$,那么他的左子树的 sizv,就等于比 $k$ 大的数中的最小数所代表节点的左子树的 sizv,所以应该往右子树搜。
对于 rkl 因为他是要找比 $k$ 大的数中的最小数的排名,所以他的返回值应为本节点的左子树的 sizv 加上自己的 num 再加 $1$,那如果 $k$ 所代表的节点的 num 为 $0$,那么他的左子树大小,就等于比他小的数中最大数所代表节点的左子树的 sizv 加上自己的 num,所以要往左子树搜。
代码如下:
1 | int rkf(int x,int k) { |
查询排名为 $k$ 的权值
没什么好说的,查看 $k$ 是否在 $(sizv_{\text{左子树}},sizv_{\text{左子树}}+num_{\text{本节点}}]$ 即可,如果小往左子树搜,大则往右子树搜。
1 | int kth(int x,int k) { |
前驱以及后继
结合一下上面的三个函数即可。
1 | int pre(int x) { return kth(rt,rkf(rt,x)); } |
插入
比较简单,递归插入即可,不多说了。
回溯时记得重构。
1 | void ins(int &x,int k) { |
删除
同上
1 | void del(int &x,int k) { |
例题
以后没准会更新,但 $99.9%$ 会咕。
板子。
1 |
|
依旧是板子(代码就不放了),记得特判第一次 $3,4,5,6$ 的操作。