替罪羊树~讲解
\(Scapegoat\)_\(Tree\)
——(替罪羊树)
——识替罪羊树之算法乃吾生之幸也!
\(First\).引子:
知乎上面有个问题问最优雅的算法是什么,我觉得
暴力即是优雅。
当然这里说的暴力并不是指那种不加以思考的无脑的暴力,而是说用繁琐而技巧性的工作可以实现的事,我用看似简单的思想和方法,也可以达到近似于前者的空间复杂度和时间复杂度,甚至可以更优,而其中也或多或少的夹杂着一些"\(Less\) \(is\) \(more\)"的思想在其中。
——摘自g1n0st
\(Second\).何为替罪羊树?
对于一棵二叉搜索树,最重要的事情就是维护它的平衡,以保证时间复杂度不要退化到\(O(N)\),保持在\(O(logN)\)左右。
为了维护树的平衡,其他的平衡二叉搜索绞尽脑汁的想了各种五花八门的方法来维护。像\(Splay\) , \(Treap\) , \(AVL\)树 , 红黑树之类几乎都是通过旋转来维护,不过是在判断旋转时的方法有不同。
而替罪羊树则是在平衡树们里的一只特立独行的猪,哦不,一棵特立独行的树。
替罪羊树是计算机科学中,一种基于部分重建的自平衡二叉搜索树。在替罪羊树上,插入或删除节点的平摊最坏时间复杂度是\(O(logN)\),搜索节点的最坏时间复杂度是\(O(logN)\)。
——摘自百度百科
那么,替罪羊树究竟特立独行在哪里。
通俗易懂简单粗暴的讲就是人家别的平衡树都几乎是通过旋转来维护平衡而我替罪羊树一言不合就直接给你拍扁了重建。(胸都给你拍平)
旋转是神魔恋?不存在的。
那么,替罪羊树为什么要叫替罪羊树。
在非平衡的二叉搜索树中,每次操作以后检查操作路径,找到最高的满足需要被重建的结点,重建整个子树。这样就得到了替罪羊树,而被重建的子树的原来的根就被称为替罪羊节点。形象的理解一下:子树要被重建不是我原来根的锅,但是我就是被拍扁了还被重建了,\(exm\)???
\(Third\).各种暴躁老哥的操作:
1.定义变量:
struct scapegoat{
int son[2], val, valid, total;//valid(有效的)未被删除的子树的点数 total(总数)子树总点数
bool exist;//是否被删除 exist(存在)1表示未被删除 0表示被删除
}e[maxn];
int memory[maxn]; //内存池
int cur[maxn]; //拍扁的时候用的内存空间
int root, pool, poi, cnt, to_rebuild;//pool指向内存池memory[]的指针,poi指向拍扁时用的cur[]的指针
注意的是我们要手写一个内存池来分配空间,动态分配内存的速度可以说是非常慢了,所以我们要手写内存池!!
2.判断是否要暴躁一下:
il bool isbad(int now)
{
if((double)e[now].valid*alpha <= (double)max(e[e[now].son[0]].valid, e[e[now].son[1]].valid)) return true;
return false;
}
因为是重量平衡树,所以我们需要知道现在的树是不是沉到我们该重建了,于是这里我们引入了一个alpha因子,一般在0.5~1.0我们取0.8 , 0.7就好惹~取大取小都不好,要么会退化的接近于一条链要么拍扁的次数会太多。(哇你光拍扁我心疼人家一下下不好么)
是否暴躁平衡的判断条件:
如果一棵树的左子树/右子树的存在的节点数量 > 这棵树的存在的节点数量*\(alpha\),那么就要重构这棵树。
3.建树&&重建
俗话说:一图胜千言。
很明显这是一棵需要重构的子树。那我们就把它拍扁吧。
可以看到拍扁后的序列其实是已经排好序的,这个顺序就是我们对这棵需要重建的子树的中序遍历的顺序。所以我们\(dfs\)一遍。
void dfs(int now) // 中序遍历,找出要被拍扁的节点的编号
{
if(!now) return;
dfs(e[now].son[0]);
if(e[now].exist) cur[++poi] = now;//加入到拍扁的时候用的数组里存放
else memory[++pool] = now;
dfs(e[now].son[1]);
}
既然拍扁了那就重建吧qaq
il void rebuild(int &now)
{
poi = 0;//别忘了你重建的子树要从头开始算啊,不清零..就听取WA声一片
dfs(now);//中序遍历一遍
if(poi) build(1,poi,now);
else now = 0;
}
你不要忘了还有建树环节呢qwq
void build(int l, int r, int &now) //你建树值要跟着变的...now当然要加&了...
{
int mid = l+r>>1;//其实建树的序列已经按顺序保存在cur里了,你只需要改变父子关系就行
now = cur[mid];//cur里存的是编号.把中间的元素取出来,中间元素的编号为now.
if(l == r)
{
e[now].son[0] = e[now].son[1] = 0;
e[now].total = e[now].valid = 1;
return;
}
if(l < mid) build(l,mid-1,e[now].son[0]);//mid已经建完了
else e[now].son[0] = 0;
build(mid+1,r,e[now].son[1]);//左右递归建树
e[now].total = e[e[now].son[0]].total + e[e[now].son[1]].total + 1;//更新节点信息
e[now].valid = e[e[now].son[0]].valid + e[e[now].son[1]].valid + 1;
}
4.插入&&删除
插入时与Splay不同,替罪羊树只需要一步一步向下找个位置插进去就是咯。
void insert(int &now, int val)
{
if(!now)//找到一个插入的位置
{
now = memory[pool--]; e[now].val = val;
e[now].exist = e[now].total = e[now].valid = 1;
e[now].son[0] = e[now].son[1] = 0;
return;
}
e[now].total++, e[now].valid++;//一边向下一边更新,这点与spaly不同
if(e[now].val >= val) insert(e[now].son[0], val);
else insert(e[now].son[1], val);
if(isbad(now)) rebuild(now);
if(!isbad(now))//插入的时候别忘了插太多会很暴躁的...
{
if(to_rebuild)
{
if(e[now].son[0] == to_rebuild) rebuild(e[now].son[0]);
else rebuild(e[now].son[1]);
to_rebuild = 0;
}
}
else to_rebuild = now;//to_rebuild是记录要重建时的节点
}
在替罪羊树中,所谓的删除也并不是真正意义上的删除而是打上标记的惰性删除。
il void delete_pos(int &now, int tar)//target(目标)删除排名为tar的数
{
if(e[now].exist&&e[e[now].son[0]].valid+ 1 == tar)//删除位置为tar的..
{
e[now].exist = 0; e[now].valid--; return;//找到了就删掉
}
e[now].valid--;
if(e[e[now].son[0]].valid + e[now].exist >= tar) delete_pos(e[now].son[0], tar);
else delete_pos(e[now].son[1],tar-e[e[now].son[0]].valid-e[now].exist);
}
il void delete_val(int tar)//删除值为tar的数
{
delete_pos(root, find_rank(tar));
if((double)e[root].total*alpha > e[root].valid) rebuild(root);//删太多了也重建一下
}
5.查找排名为k的数和数为k的排名
查找排名为k的数,kth
il int find_kth(int k)
{
int now = root;
while(now)
{
if(e[now].exist&&e[e[now].son[0]].valid+1 == k) return e[now].val;//找到了,返回
else if(e[e[now].son[0]].valid >= k) now = e[now].son[0];//往左找
else//往右找
{
k -= e[e[now].son[0]].valid + e[now].exist;
now = e[now].son[1];
}
}
}
都是二叉搜索树的操作吧...左小右大的原则。
il int find_rank(int k)//寻找k的排名
{
int now = root;
int ans = 1;
while(now)
{
if(e[now].val >= k) now = e[now].son[0];
else
{
ans += e[e[now].son[0]].valid + e[now].exist;//+e[now].exist是因为我相同大小的节点虽然放在一起,但是我不知道我这个节点上相同的我是不是还存在啊..所以我得单独加我..至于valid是除我以外的子树大小。
now = e[now].son[1];
}
}
return ans;
}
至此,替罪羊树的一些基本操作结束啦~
\(Fourth\).\(Code\):
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define alpha 0.75
#define maxn 4000001
#define ri register
#define il inline
using namespace std;
inline int read()
{
int k=0,f=1;
char c=getchar();
while(!isdigit(c))
{
if(c=='-')f=-1;
c=getchar();
}
while(isdigit(c))
{
k=(k<<1)+(k<<3)+c-48;
c=getchar();
}
return k*f;
}
struct scapegoat{
int son[2], val, valid, total;
bool exist;
}e[maxn];
int memory[maxn];
int cur[maxn];
int root, pool, poi, cnt, to_rebuild;
il bool isbad(int now)
{
if((double)e[now].valid*alpha <= (double)max(e[e[now].son[0]].valid, e[e[now].son[1]].valid)) return true;
return false;
}
void dfs(int now)
{
if(!now) return;
dfs(e[now].son[0]);
if(e[now].exist) cur[++poi] = now;
else memory[++pool] = now;
dfs(e[now].son[1]);
}
void build(int l, int r, int &now)
{
int mid = l+r>>1;
now = cur[mid];
if(l == r)
{
e[now].son[0] = e[now].son[1] = 0;
e[now].total = e[now].valid = 1;
return;
}
if(l < mid) build(l,mid-1,e[now].son[0]);
else e[now].son[0] = 0;
build(mid+1,r,e[now].son[1]);
e[now].total = e[e[now].son[0]].total + e[e[now].son[1]].total + 1;
e[now].valid = e[e[now].son[0]].valid + e[e[now].son[1]].valid + 1;
}
il void rebuild(int &now)
{
poi = 0;
dfs(now);
if(poi) build(1,poi,now);
else now = 0;
}
il int find_rank(int k)
{
int now = root;
int ans = 1;
while(now)
{
if(e[now].val >= k) now = e[now].son[0];
else
{
ans += e[e[now].son[0]].valid + e[now].exist;
now = e[now].son[1];
}
}
return ans;
}
il int find_kth(int k)
{
int now = root;
while(now)
{
if(e[now].exist&&e[e[now].son[0]].valid+1 == k) return e[now].val;
else if(e[e[now].son[0]].valid >= k) now = e[now].son[0];
else
{
k -= e[e[now].son[0]].valid + e[now].exist;
now = e[now].son[1];
}
}
}
void insert(int &now, int val)
{
if(!now)
{
now = memory[pool--]; e[now].val = val;
e[now].exist = e[now].total = e[now].valid = 1;
e[now].son[0] = e[now].son[1] = 0;
return;
}
e[now].total++, e[now].valid++;
if(e[now].val >= val) insert(e[now].son[0], val);
else insert(e[now].son[1], val);
if(isbad(now)) rebuild(now);
if(!isbad(now))
{
if(to_rebuild)
{
if(e[now].son[0] == to_rebuild) rebuild(e[now].son[0]);
else rebuild(e[now].son[1]);
to_rebuild = 0;
}
}
else to_rebuild = now;
}
il void delete_pos(int &now, int tar) //target(目标)
{
if(e[now].exist&&e[e[now].son[0]].valid+ 1 == tar)
{
e[now].exist = 0; e[now].valid--; return;
}
e[now].valid--;
if(e[e[now].son[0]].valid + e[now].exist >= tar) delete_pos(e[now].son[0], tar);
else delete_pos(e[now].son[1],tar-e[e[now].son[0]].valid-e[now].exist);
}
il void delete_val(int tar)
{
delete_pos(root, find_rank(tar));
if((double)e[root].total*alpha > e[root].valid) rebuild(root);
}
int main()
{
int opt, x, m;
for(ri int i = 4000000; i >= 1; i--) memory[++pool] = i;
m = read();
while(m--)
{
opt = read(); x = read();
if(opt == 1) {insert(root, x);}
if(opt == 2) {delete_val(x);}
if(opt == 3) {printf("%d\n",find_rank(x));}
if(opt == 4) {printf("%d\n",find_kth(x));}
if(opt == 5) {printf("%d\n",find_kth(find_rank(x)-1));}
if(opt == 6) {printf("%d\n",find_kth(find_rank(x+1)));}
}
return 0;
}
\(Fifth\).后记:
——关于平衡树和这篇文章的二三事
在贴吧里看到了关于各类平衡树的比较:
关于这篇文章也关于这棵替罪羊树:
替罪羊树是我自学的,所以在文章里或许会有些个人理解上的偏差,还请各位dalao能赐教。自学能力是需要培养、锻炼的。
其次是也引用了一些dalao写的很好的文章,也是我自学时用到的资料:
祝各位OI路途能越走越顺!
本蒟蒻QQ 935145183/3203600070
隐约雷鸣,阴霾天空,但盼风雨来,能留你在此。
隐约雷鸣,阴霾天空,即使天无雨,我亦留此地。