替罪羊树

总结:

1、伸展树靠不停的旋转来保持平衡,treap的话用一个随机的东西保持平衡,而替罪羊树直接把不平衡的子树拍平,直接暴力重构来平衡

2、重构允许重构整棵替罪羊树,也允许重构替罪羊树其中的一棵子树。

3、替罪羊树可以和kd-tree结合使用

 

详解:

0x00 扯淡

知乎上面有个问题问最优雅的算法是什么,我觉得暴力即是优雅

当然这里说的暴力并不是指那种不加以思考的无脑的暴力,而是说用繁琐而技巧性的工作可以实现的事,我用看似简单的思想和方法,也可以达到近似于前者的空间复杂度和时间复杂度,甚至可以更优,而其中也或多或少的夹杂着一些"LESS IS MORE"的思想在其中。

以下文章需要对普通二叉搜索树Treap树(可选)有一定的了解,可以自行百度也可以等我出的一篇有关这个的文章。

0x01 替罪羊树[Scapegoat Tree]

对于一棵二叉搜索树,最重要的事情就是维护他的平衡,以保证对于每次操作(插入,查找,删除)的时间均摊下来都是替罪羊树_数据乃至替罪羊树_复杂度_02红黑树,但是常数大而且难写,此处不展开介绍)。

为了维护树的平衡,各种平衡二叉树绞尽脑汁方法五花八门,但几乎都是通过旋转的操作来实现(AVL 树红黑树Treap 树(经@GadyPu 指正,可持久化Treap树不需要旋转) Splay…),只不过是在判断什么时候应该旋转上有所不同。但替罪羊树就是那么一棵特立独行的猪,哦不,是一只特立独行的树。

替罪羊树_子树_03

0x02 各种嘿嘿嘿的操作

  • 重构

重构允许重构整棵替罪羊树,也允许重构替罪羊树其中的一棵子树。

重构这个操作看似高端,实则十分暴力(真)。主要操作就是把需要重构的子树拍平(由于子树一定是二叉搜索树,所以拍平之后的序列一定也是有序的),然后拎起序列的中点,作为根部,剩下的左半边序列为左子树,右半边序列为右子树,接着递归对左边和右边进行同样的操作,直到最后形成的树中包含的全部为点而不是序列(这样形成的一定是一棵完全二叉搜索树,也是最优的方案)。

替罪羊树_数据_04

 

这是一棵需要维护的子树,虽然目前不知道基于什么判断条件,但这棵是明显需要维护的。。替罪羊树_复杂度_05

替罪羊树_子树_06拍平之后的结果,直接遍历即可。

替罪羊树_复杂度_07

 

子树的重构就完成了。

  • 插入

插入操作一开始和普通的二叉搜索树无异,但在插入操作结束以后,从插入位置开始一层一层往上回溯的时候,对于每一层都进行一次判断替罪羊树_二叉搜索树_08,一直找到最后一层不满足该条件的层序号(也就是从根开始的第一层不满足该条件的层序号),然后从该层开始重构以该层为根的子树(一个节点导致树的不平衡,就要导致整棵子树被拍扁,估计这也是“替罪羊”这个名字的由来吧)。

每次插入操作的复杂度为替罪羊树_子树_09,每次重构树的复杂度为替罪羊树_数据_10,但由于不会每次都要进行重构,也不会每次都重构一整棵树,所以均摊下来的复杂度还是替罪羊树_取值_11

替罪羊树_复杂度_12在这里是一个常数,可以通过调整替罪羊树_复杂度_13的大小来控制树的平衡度,使程序具有很好的可控性。

-------------替罪羊树_子树_14日更新-------------

为了测试替罪羊树_二叉搜索树_15值的选取对于程序性能的影响,枚举了替罪羊树_取值_16这个区间内替罪羊树_数据_17的值,性能绘制成图标如下(数据采用BZOJ 6,7,8三组数据的3倍)

 

替罪羊树_子树_18(测试结果如上)

 

由此可见,替罪羊树_二叉搜索树_19区间内替罪羊树_子树_20的取值对于程序性能并没有很大的影响,当然也有可能是我测试方法不当,

-------------替罪羊树_复杂度_21日更新-------------

​@dashgua​​,把测试数据进行了更改,全部改为1000000个节点按次序插入和逆序删除。

替罪羊树_取值_22

 

(测试结果如上)

对于取值越靠近两端的确速度越慢,但中间貌似还是没有什么差异。如果有好的数据构造方法希望能提出,一定会再次尝试,谢谢。

  • 删除(惰性删除)

我觉得删除操作是替罪羊树中最好玩的地方,替罪羊树的删除节点并不是真正的删除,而是惰性删除(即给节点增加一个已经删除的标记,删除后的节点与普通节点无异,只是不参与查找操作而已)。当删除的数量超过树的节点数的一半时,直接重构!(屌丝和暴力属性替罪羊树_子树_23),可以证明均摊下来的复杂度还是替罪羊树_取值_24(作者太傻证明不来)。

  • 查找第K大&查找数X的序号

和普通的二叉搜索树无异,但是需要注意标明被删除掉的节点不能被算入。

0x03 代码

以下是替罪羊树的模板,大部分操作直接调用成员函数就可以了。



#include <vector>
using namespace std;

namespace Scapegoat_Tree {
#define MAXN (100000 + 10)
const double alpha = 0.75;
struct Node {
Node * ch[2];
int key, size, cover; // size为有效节点的数量,cover为节点总数量
bool exist; // 是否存在(即是否被删除)
void PushUp(void) {
size = ch[0]->size + ch[1]->size + (int)exist;
cover = ch[0]->cover + ch[1]->cover + 1;
}
bool isBad(void) { // 判断是否需要重构
return ((ch[0]->cover > cover * alpha + 5) ||
(ch[1]->cover > cover * alpha + 5));
}
};
struct STree {
protected:
Node mem_poor[MAXN]; //内存池,直接分配好避免动态分配内存占用时间
Node *tail, *root, *null; // 用null表示NULL的指针更方便,tail为内存分配指针,root为根
Node *bc[MAXN]; int bc_top; // 储存被删除的节点的内存地址,分配时可以再利用这些地址

Node * NewNode(int key) {
Node * p = bc_top ? bc[--bc_top] : tail++;
p->ch[0] = p->ch[1] = null;
p->size = p->cover = 1; p->exist = true;
p->key = key;
return p;
}
void Travel(Node * p, vector<Node *>&v) {
if (p == null) return;
Travel