其他分享
首页 > 其他分享> > 替罪羊树学习笔记

替罪羊树学习笔记

作者:互联网

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);//重构
}

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
```cpp
#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;
	}
}

标签:val,tsi,int,替罪羊,tree,笔记,学习,ls,now
来源: https://www.cnblogs.com/thirty-two/p/14315461.html