• 替罪羊树学习笔记


    Part 0 引子

    我们都知道,有一种东西叫 BST。
    我们都知道,BST 在极限数据会卡爆。
    我们都知道,为了让 BST 不被卡,有很多种平衡树。
    但你知道有一种平衡树好写速度快吗?那就是替罪羊树。

    Part 1 替罪羊树平衡的原理

    替罪羊树是一种平衡树,一种平衡的 BST。
    什么?你连 BST 都不会?建议学完 BST 再来看替罪羊。
    我们以这题为例讲解。
    先放一个最基础的数组。

    struct node{
    		int ls,rs,siz,tsi,val,cnt;//ls rs 左右儿子,siz 子树大小,tsi 有效子树大小,val 权值,cnt 这个点的重复次数
    	}tree[100010];
    

    Part 2 替罪羊树的核心原理

    BST 在极端数据的时候,可以被卡成:

    就很不好了。
    别的平衡树都是旋转,我替罪羊不是,我替罪羊是平衡树中一只特立独行的树,是靠着拍扁重构的。
    还是拿上面的那个图举例子,我们假装这是一棵大树的子树。
    替罪羊树维护平衡,主要就是一个 (alpha) 因子,这个因子的意思是:
    这个点的大小乘上个 (alpha) 是不是小于我某个左右儿子
    如果成立,这个点会大喊一声:“我劝你耗子尾汁,你不对劲”拍扁重构!


    拍扁重构是一个非常暴力的事情。
    我们是都知道的,BST 中序遍历后是一个有序数组。
    那么有了这个有序数组我们可以干嘛呢?我们每次先找到中间的值,然后左半边是它的左子树,右半边是他的右子树,然后对于左半边和右半边,我们依然按照这样处理,直到叶子节点。显然,这样划分每个层都得到了充分的利用,树高 (log_2 n)
    所以……替罪羊树就是这样干一遍…
    就很恐怖。
    代码:

    void traverse(int now)//中序遍历
    {
    	if(tree[now].ls)traverse(tree[now].ls);
    	if(tree[now].cnt)rest[++S]=now;
    	if(tree[now].rs)traverse(tree[now].rs);
    	return ;
    }
    void updata(int now)//更新该点信息
    {
    	tree[now].siz=tree[tree[now].ls].siz+tree[tree[now].rs].siz+tree[now].cnt;
    	tree[now].tsi=tree[tree[now].ls].tsi+tree[tree[now].rs].tsi+tree[now].cnt;
    }
    int build(int l,int r)
    {
        if(l==r)return 0;
        int mid=(l+r)>>1;//中间的是根
        tree[rest[mid]].ls=build(l  ,mid);
        tree[rest[mid]].rs=build(mid+1,r);
        updata(rest[mid]);
        return rest[mid];
    } 
    void Start_build(int &k)
    {
    	S=0;//S 是内存池下标
    	traverse(k);//遍历
    	k=build(1,S+1);//重构
    }
    

    一般来说,(alpha) 我们回取 (0.75)

    Part 3 替罪羊树的经典操作

    Part 3.1 插入

    替罪羊树的插入和 BST 一样。
    假设我们现在找到了 (now) 号节点,分 (4) 种情况。

    1. 刚好要插入的 (val)(now) 号节点的 (val) 一样,就把 (now)(cnt+1)
    2. (val)(now)(val) 小,到 (now) 的左子树转悠。
    3. (val)(now)(val) 大,到 (now) 的右子树转悠。
    4. 这个点空着,那就给 (val) 八~

    记得插完了以后判断重构呀。

    void T_insert(int &now,int val)
    {
    	if(!now)//4
    	{
    		now=++T_s;
    		if(!root)root=1;
    		tree[now].val=val,tree[now].ls=tree[now].rs=0;
    		tree[now].cnt=tree[now].siz=tree[now].tsi=1;	
    	}
    	else
    	{
    		if(tree[now].val==val)tree[now].cnt++;//1
    		else if(tree[now].val>val)T_insert(tree[now].ls,val);//2
    		else T_insert(tree[now].rs,val);//3
    		updata(now);//updata 用于更新子树大小
    		if(check(now))Start_build(now);
    	}
    }
    

    Part 3.2 删除

    删除是替罪羊树最好玩的操作。
    如果我们删掉了这个节点,那这个节点的儿子们该何去何从?
    所以,我们打上个标记就好了~
    什么?这么简单?
    没错,就这么简单/cy
    这就是定义的时候有一个 (tsi) 了,这个代表有效节点个数。
    具体实现有 (4) 步。

    1. 删除 (now) 的有效节点数。
    2. (val)(now)(val),一样,减去 (cnt)
    3. (val) 小于 (now)(val),就去左子树转悠。
    4. (val) 大于 (now)(val),就到右子树转悠。
    void T_delete(int &now,int val)
    {
    	tree[now].tsi--;
    	if(tree[now].val==val)tree[now].cnt--;
    	else if(tree[now].val>val)T_delete(tree[now].ls,val);
    	else T_delete(tree[now].rs,val);
    	updata(now);
    	if(check(now))Start_build(now);
    }
    

    Part 3.3 按排名找值

    这个我个人认为是替罪羊树最难的操作(大雾
    老样子,四步走(为什么都是四步)

    1. (now) 的左右儿子相等,走不下去了,就返回。
    2. 左儿子够用,就在左儿子里面递归。
    3. 单靠左儿子不够用,但是加上根节点的 (cnt) 就够用了,说明卡在了根节点上,就返回。
    4. 只能靠右儿子了,这个时候,我们默认左儿子用完了,所以递归的时候就把 (k) 减去这个左儿子的子树。

    注意:此处使用的子树是 (tsi),不是 (siz)

    int T_findv(int now,int k)
    {
    	if(tree[now].ls==tree[now].rs)return tree[now].val;
    	else if(tree[tree[now].ls].tsi>=k)return T_findv(tree[now].ls,k);
    	else if(k>tree[tree[now].ls].tsi&&tree[tree[now].ls].tsi+tree[now].cnt>=k)return tree[now].val;
    	else return T_findv(tree[now].rs,k-tree[tree[now].ls].tsi-tree[now].cnt);
    }
    

    Part 3.4 按值找排名

    这个操作也很好玩。
    分类讨论。
    1.当前 (now) 的值和要查询的一样,就加上比 (val) 小的,也就是左子树大小
    2.当前 (now) 的值比要查询的大了,就去左子树找,因为左子树小。
    3.当前 (now) 的值比要查询的小了,首先左子树和该点本身的 (cnt) 一定要加上,然后还有右子树的也要找一下。

    int T_findr(int now,int val)
    {
    	if(!now)return 0;
    	if(tree[now].cnt&&tree[now].val==val)return tree[tree[now].ls].tsi;
    	else if(val<tree[now].val)return T_findr(tree[now].ls,val);
    	else return tree[tree[now].ls].tsi+tree[now].cnt+T_findr(tree[now].rs,val);
    }
    

    注意,这段找的实际上是排名 -1,或者说是比 (val) 小的

    Part 3.5 前驱后继

    前驱后继实在太简单了,就放到一块儿写了。
    前驱就是找小的,我们上面的函数是找 (val) 小的,刚刚好,在按排名找值周套一个找比 (val) 小的。
    后继就是找大的,我们要知道最小大于 (val) 的排名是多少,显然是 (val+1) 的排名。
    然后按排名找值就完了。
    注意,按排名找值是从 1 开始计数,而按值找排名是从 0 开始计数,一定要注意。这样的小细节请大家自行思考。

    int upper(int val)
    {
          return T_findv(root,T_findr(root,val));
    }
    int lower(int val)
    {
    	return T_findv(root,T_findr(root,val+1)+1);
    }
    

    Part 3.6 完整代码

    用 struct 封装的,码风有点丑,见谅 qwq

    #include<iostream>
    #include<algorithm>
    using namespace std;
    double alpha=0.75;
    int rest[100010]; 
    struct Scapegoat_Tree{
    	int S;//内存池下标
    	int root; 
    	int T_s;
    	struct node{
    		int ls,rs,siz,tsi,val,cnt;
    	}tree[100010];
    	void first()
    	{
    		S=root=0;
    	}
    	void test(int now)
    	{
    		if(tree[now].ls)test(tree[now].ls);
    		cout<<tree[now].val<<' ';
    		if(tree[now].rs)test(tree[now].rs);
    		/*for(int p=0;p<=5;p++)
    			cout<<tree[p].ls<<' '<<tree[p].rs<<' '<<tree[p].siz<<' '<<tree[p].tsi<<' '<<tree[p].val<<' '<<tree[p].cnt<<endl;*/ 
    	}
    	bool check(int now)
    	{
    		if(tree[now].cnt)
    			if(double(max(tree[tree[now].ls].siz,tree[tree[now].rs].siz))>alpha*double(tree[now].tsi))return true;
    			else return false; 
    	}
    	void traverse(int now)
    	{
    		if(tree[now].ls)traverse(tree[now].ls);
    		if(tree[now].cnt)rest[++S]=now;
    		if(tree[now].rs)traverse(tree[now].rs);
    		return ;
    	}
    	void updata(int now)
    	{
    		tree[now].siz=tree[tree[now].ls].siz+tree[tree[now].rs].siz+tree[now].cnt;
    		tree[now].tsi=tree[tree[now].ls].tsi+tree[tree[now].rs].tsi+tree[now].cnt;
    	}
    	int build(int l,int r)
    	{
    	    if(l==r)return 0;
    	    int mid=(l+r)>>1;
    	    tree[rest[mid]].ls=build(l  ,mid);
    	    tree[rest[mid]].rs=build(mid+1,r);
    	    updata(rest[mid]);
    	    return rest[mid];
    	} 
    	void Start_build(int &k)
    	{
    		S=0;
    		traverse(k);
    		k=build(1,S+1);
    	}
    	void T_insert(int &now,int val)
    	{
    		if(!now)
    		{
    			now=++T_s;
    			if(!root)root=1;
    			tree[now].val=val,tree[now].ls=tree[now].rs=0;
    			tree[now].cnt=tree[now].siz=tree[now].tsi=1;	
    		}
    		else
    		{
    			if(tree[now].val==val)tree[now].cnt++;
    			else if(tree[now].val>val)T_insert(tree[now].ls,val);
    			else T_insert(tree[now].rs,val);
    			updata(now);
    			if(check(now))Start_build(now);
    		}
    	}
    	void T_delete(int &now,int val)
    	{
    		tree[now].tsi--;
    		if(tree[now].val==val)tree[now].cnt--;
    		else if(tree[now].val>val)T_delete(tree[now].ls,val);
    		else T_delete(tree[now].rs,val);
    		updata(now);
    		if(check(now))Start_build(now);
    	}
    	int T_findv(int now,int k)
    	{
    		if(tree[now].ls==tree[now].rs)return tree[now].val;
    		else if(tree[tree[now].ls].tsi>=k)return T_findv(tree[now].ls,k);
    		else if(k>tree[tree[now].ls].tsi&&tree[tree[now].ls].tsi+tree[now].cnt>=k)return tree[now].val;
    		else return T_findv(tree[now].rs,k-tree[tree[now].ls].tsi-tree[now].cnt);
    	}
    	int T_findr(int now,int val)
    	{
    		if(!now)return 0;
    		if(tree[now].cnt&&tree[now].val==val)return tree[tree[now].ls].tsi;
    		else if(val<tree[now].val)return T_findr(tree[now].ls,val);
    		else return tree[tree[now].ls].tsi+tree[now].cnt+T_findr(tree[now].rs,val);
    	}
    	int upper(int val)
    	{
    		return T_findv(root,T_findr(root,val));
    	}
    	int lower(int val)
    	{
    		return T_findv(root,T_findr(root,val+1)+1);
    	}
    }Tree; 
    int main()
    {
    	//freopen(".in","r",stdin);
    	//freopen(".out","w",stdout);
    	ios::sync_with_stdio(false);
    	Tree.first();
    	int m,opt,x;
    	cin>>m;
    	while(m--)
    	{
    		cin>>opt>>x;
    		if(opt==1)Tree.T_insert(Tree.root,x);
    		else if(opt==2)Tree.T_delete(Tree.root,x);
    		else if(opt==3)cout<<Tree.T_findr(Tree.root,x)+1<<endl;
    		else if(opt==4)cout<<Tree.T_findv(Tree.root,x)<<endl;
    		else if(opt==5)cout<<Tree.upper(x)<<endl;
    		else if(opt==6)cout<<Tree.lower(x)<<endl;
    	}
    }
    
  • 相关阅读:
    spring ConfigurationProperties 注解
    MySQL安装
    Linux虚机密码破解
    spring cloud zuul 配置(Robbin 和 熔断)
    Oracel官网下载各类版本的JDK
    spring @Configuration
    IDEA debug
    Spring Boot @ControllerAdvice+@ExceptionHandler处理controller异常
    redis-day1
    Mysql进阶-day3
  • 原文地址:https://www.cnblogs.com/thirty-two/p/14315461.html
Copyright © 2020-2023  润新知