替罪羊树
-
-
大佬1的理解和模版:https://zhuanlan.zhihu.com/p/21263304,适合理解替罪羊树的原理,但是没有考虑很多点值都相等的情况,所有的点都是分开的,这点不是很好。
-
大佬2的理解和模版:https://www.cnblogs.com/tlx-blog/p/12900730.html,考虑了不同点值相等的情况,都合在一起,每个函数讲解得很好。
-
-
简介:
-
名称:替罪羊树(Scapegoat Tree)
-
本质:优雅的暴力,用少量简单的代码,实现平衡树等能做的事情,经常做到“暴力碾标算”的一种神奇的数据结构~
-
一些abstract:
对于一棵二叉搜索树,最重要的事情就是维护他的平衡,以保证对于每次操作(插入,查找,删除)的时间均摊下来都是O(logN)。
而为了维护树的平衡,各种平衡二叉树方法大同小异,本质几乎都是通过旋转的操作来实现(AVL树,红黑树,Splay,Treap(可持久化Treap不需要旋转)),只是在判断什么时候应该旋转上有所不同。但是替罪羊树不同,人家不转也能玩得转x。
-
-
核心操作:
-
重构:可以重构整棵树,也可以重构子树。操作其实十分暴力,就是把需要重构的树拍平成一条链,显然这条链一定是有序的~。最后把这条链的中点拽起来,作为新的树的根,左边的序列作为左子树,右边的序列作为右子树,递归实现即可。
-
插入:
-
一些细节:
一开始和普通的二叉搜索树一样,但是插入操作结束以后,从插入位置开始一层一层往上回溯,每一层都进行如下判断:
h(v) > log(1/α) × size(tree)(不同人的式子不一样,但是大同小异吧,这里h(v)为左右子树中高度的max,α为一个常数,介于0.5到1,通常取0.7、0.8、0.75,size就是树的节点个数啦~)
然后一直找到最后不满足这个式子的那一层(就是即将大于右边的那一层,也可以理解为从根节点到这个点的路径中,第一个导致高度不大于右侧的点),也就是h(v)每次加一,早晚会大于右侧,然后重构这一层的子树。
-
关于复杂度:
每次插入的复杂度显然和别的树一样,为O(logn),每次重构树大概是O(n)级别(吃瓜群众:那不炸了吗?你在说什么?!)。但因为不是每次都要重构一整颗树,玄学地均摊下来,复杂度也是O(logn)(有一种平衡树中平衡因子的复杂度的玄学感x),并不用担心复杂度爆炸。
-
胡扯阶段:
有趣的是,因为一个节点导致整个子树被拍扁,然后根节点被提了起来,根节点成了最后的“替罪羊”,这大概就是“替罪羊”名字的由来吧。
-
-
删除:
-
删除节点不是真正的删除,而是打标记的惰性删除,打上标记的节点具有如下特征:
-
不参与查找操作
-
参与其余操作
-
当删除的数量超过树节点数的一半(这个也可以变化),重构这棵树。
-
-
关于复杂度:仍然是O(logn),不会证明,知道就行。
-
-
查找第k大的值,以及给一个树查看它的rank:和其余树没什么区别,只是不能计算删除节点。
-
-
代码:搬用https://zhuanlan.zhihu.com/p/21263304的代码,这里只学此数据结构的思想,加了注释(主食x),应该能懂吧。
#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(p->ch[0], v); if (p->exist) v.push_back(p); // 构建序列 else bc[bc_top++] = p; // 回收 Travel(p->ch[1], v); } Node * Divide(vector<Node *>&v, int l, int r) { // 根据序列得到每个节点的儿子信息 if (l >= r) return null; int mid = (l + r) >> 1; Node * p = v[mid]; p->ch[0] = Divide(v, l, mid); p->ch[1] = Divide(v, mid + 1, r); p->PushUp(); // 自底向上维护,先维护子树 return p; } void Rebuild(Node * &p) { // 重构包括重新拿到存活的节点,以及平衡地得到每个节点的儿子信息 static vector<Node *>v; v.clear(); Travel(p, v); p = Divide(v, 0, v.size()); } Node ** Insert(Node *&p, int val) { // 插入操作 if (p == null) { p = NewNode(val); return &null; } else { p->size++; p->cover++; // 返回值储存需要重构的位置,若子树也需要重构,本节点开始也需要重构,以本节点为根重构 Node ** res = Insert(p->ch[val >= p->key], val); if (p->isBad()) res = &p; // 如果这层也沦陷了就更新,否则就是子树的答案一直返回 return res; } } void Erase(Node *p, int id) { // 删掉第id大的 p->size--; int offset = p->ch[0]->size + p->exist; // 根节点是第几大的 if (p->exist && id == offset) { // 删就对了 p->exist = false; return; } else { if (id <= offset) Erase(p->ch[0], id); // 往左闪 else Erase(p->ch[1], id - offset); // 往右删 } } public: void Init(void) { tail = mem_poor; null = tail++; null->ch[0] = null->ch[1] = null; null->cover = null->size = null->key = 0; root = null; bc_top = 0; } STree(void) { Init(); } void Insert(int val) { Node ** p = Insert(root, val); if (*p != null) Rebuild(*p); // 拿到rebuild的位置,开始重构 } int Rank(int val) { // 拿到val值的排名,如果有重复的取拍平序列的第一次出现的位置 Node * now = root; int ans = 1; while (now != null) { // 非递归求排名 if (now->key >= val) now = now->ch[0]; else { ans += now->ch[0]->size + now->exist; now = now->ch[1]; } } return ans; } int Kth(int k) { Node * now = root; while (now != null) { // 非递归求第K大 if (now->ch[0]->size + 1 == k && now->exist) return now->key; // 只有这样才是自己 else if (now->ch[0]->size >= k) now = now->ch[0]; // 往左找 else k -= now->ch[0]->size + now->exist, now = now->ch[1]; // 往右找,别忘了更新k } } void Erase(int k) { // 删掉值为k的点 Erase(root, Rank(k)); // 但是注意 如果值为k的点不在这个树里,这样是不对的。 if (root->size < alpha * root->cover) Rebuild(root); // 重构条件 } void Erase_kth(int k) { Erase(root, k); if (root->size < alpha * root->cover) Rebuild(root); // 重构条件 } }; #undef MAXN } INLINE void read(int &x) { static char c; c = NC(); int b = 1; for (x = 0; !(c >= '0' && c <= '9'); c = NC()) if(c == '-') b = -b; for (; c >= '0' && c <= '9'; x = x * 10 + c - '0', c = NC()); x *= b; } using namespace Scapegoat_Tree; STree _t; int n, k, m; int main(void) { //freopen("in.txt", "r", stdin); //freopen("out.txt", "w", stdout); read(n); while (n--) { read(k), read(m); switch (k) { case 1: _t.Insert(m); break; case 2: _t.Erase(m); break; case 3: printf("%d\n", _t.Rank(m)); break; case 4: printf("%d\n", _t.Kth(m)); break; case 5: printf("%d\n", _t.Kth(_t.Rank(m) - 1)); break; case 6: printf("%d\n", _t.Kth(_t.Rank(m + 1))); break; } /* DEBUG INFO vector<Node *> xx; _t.Travel(_t.root, xx); cout << "::"; for(int i = 0; i < xx.size(); i++) cout << xx[i]->key << ' '; cout << endl; */ } return 0; }