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

替罪羊树学习笔记

时间:2022-12-10 11:22:17浏览次数:56  
标签:sz cnt return seq int 替罪羊 笔记 学习 ls

前言

替罪羊树(Scapegoat Tree,SGT)是由 Arne Andersson 提出的一种非旋转自平衡树,可以做到单次均摊 \(O(\log n)\) 的时间复杂度内实现平衡树的所有操作(时间复杂度基于势能分析)。

替罪羊树的优点:

  • 不需要进行左旋、右旋、单旋、双旋、伸展等基于旋转操作。减少了码量和思维量。
  • 常数相对于常用的 Splay、FHQ Treap 较小(如 P6136,FHQ Treap 19.93 s,替罪羊树 16.05 s 相差近 \(4\) 秒)。

替罪羊树的缺点:

  • 由于时间复杂度由势能分析保证,因此无法可持久化(不过似乎有人写了没被卡)
  • 无法实现分裂、合并操作,也无法实现文艺平衡树。

前置姿势:二叉搜索树。(可以认为替罪羊树就是有平衡措施的二叉搜索树)

如果不会二叉搜索树建议看 「学习笔记」浅析BST二叉搜索树 - do_while_true - 博客园 (cnblogs.com)

约定

我们使用下面的结构体来存储节点:

struct node{
	int s,sz,sd,cnt,l,r,w;
	void init(int weight){
		w=weight;
		l=r=0;
		s=sz=sd=cnt=1;
	}
} t[10000005];

其中:

\(\texttt{s}\) 表示以当前节点为根的子树大小(不计重复元素,计删除了却保留下来的元素)(维护平衡使用)

\(\texttt{sz}\) 表示以当前节点为根的子树大小(计重复元素,计删除了却保留下来的元素)(基本操作使用)

\(\texttt{sd}\) 表示以当前节点为根的子树大小(不计重复元素,不计删除了却保留下来的元素)(维护平衡使用)

\(\texttt{cnt}\) 表示当前节点的元素个数。

\(\texttt{l,r}\) 分别表示当前节点的左、右子节点。

\(\texttt{w}\) 表示当前节点保存的值。

\(\texttt{init(weight)}\) 函数表示新建一个值为 \(\texttt{weight}\) 的节点的逻辑。

除此之外,我们还定义以下宏、变量:

#define ls (t[i].l)
#define rs (t[i].r)
int root,tot;
const double alpha=0.7;

\(\texttt{ls,rs}\) 的意义不必多说,\(\texttt{root}\) 是当前的根,\(\texttt{tot}\) 是当前已经有过多少个节点。

\(\alpha(\texttt{alpha})\) 表示的是平衡因子,在后面会有介绍。

信息上推

我们需要从子节点推出父节点的信息,就需要使用信息上推(Push Up)。

代码实现非常自然,就偷个懒,不讲了,代码如下:

void pushup(int i){
	t[i].s=t[ls].s+t[rs].s+1;
	t[i].sz=t[ls].sz+t[rs].sz+t[i].cnt;
	t[i].sd=t[ls].sd+t[rs].sd+(t[i].cnt>0);
}

维护平衡

总述

替罪羊树是基于”重构“(Rebuild)来实现平衡的。

所谓重构,不过是将子树打破直接暴力建出新树而已。同时还会不保留删除过的节点(\(\texttt{cnt}=0\))。

重构的契机

知道了什么是重构,接下来我们来想一想什么时候重构?

  • 如果一个节点的子树大小 \(s_1\) 占到了这个节点的子树大小 \(s\) 的大部分,那么就需要重构这个节点。
  • 如果一个节点的子树中未被删除的节点数 \(\texttt{sz}\) 占到了这个节点的子树大小 \(s\) 的小部分,那么就需要重构这个节点。

具体的大部分、小部分是多少呢?我们用 \(\alpha\)(平衡因子)来定义,可以像这样:

  • 如果 \(\min\{s_{l},s_{r}\}\geq\alpha\cdot s\),那么就需要重构这个节点。
  • 如果 \(\texttt{sz}\leq\alpha\cdot s\),那么就需要重构这个节点。

容易看出 \(\alpha\) 需要满足 \(\frac{1}{2}\leq\alpha\lt1\)。在实际应用中,一般取 \(0.7\) 或 \(0.8\)。

然后我们就可以写出一个判断函数:

bool need_rebuild(int i){
	if(t[i].cnt==0)return false;
	if(alpha*t[i].s<=(double)(max(t[ls].s,t[rs].s)))return true;
	if((double)t[i].sd<=alpha*t[i].s)return true;
	return false;
}

下面可能会将【需要重构】称之为【失衡】。

重构——平展

平展(Flatten)是重构中的前半部分,它指的是将一个子树按中序遍历展开。求中序遍历。

Flatten 在英文中还有精简的意思。在平展过程中我们也需要精简节点。就是将删完的节点(\(\texttt{cnt}=0\))从中序遍历中删除。

代码:

void flatten(int i,vector<int> &seq){
	if(!i)return;
	flatten(ls,seq);
	if(t[i].cnt)seq.push_back(i);
	flatten(rs,seq);
}

重构——建立

建立(Build)就是给出中序遍历,建出二叉搜索树。当然我们需要让建出来的数尽量平衡,只需要每一次选择区间 \([l,r]\) 的中部即可。具体细节如果不会,建议重新从普及组学起。

代码如下:

int build(int l,int r,const vector<int> &seq){
	if(l==r)return 0;
	int mid=(l+r)>>1;
	int i=seq[mid];
	ls=build(l,mid,seq);
	rs=build(mid+1,r,seq);
	pushup(i);
	return i;
}

重构实现

然后就成功实现重构部分了!贴个整体代码:

bool need_rebuild(int i){
	if(t[i].cnt==0)return false;
	if(alpha*t[i].s<=(double)(max(t[ls].s,t[rs].s)))return true;
	if((double)t[i].sd<=alpha*t[i].s)return true;
	return false;
}

namespace Rebuild{
	void flatten(int i,vector<int> &seq){
		if(!i)return;
		flatten(ls,seq);
		if(t[i].cnt)seq.push_back(i);
		flatten(rs,seq);
	}
	int build(int l,int r,const vector<int> &seq){
		if(l==r)return 0;
		int mid=(l+r)>>1;
		int i=seq[mid];
		ls=build(l,mid,seq);
		rs=build(mid+1,r,seq);
		pushup(i);
		return i;
	}
}

void rebuild(int &i){
	vector<int> seq;
	seq.push_back(0);
	Rebuild::flatten(i,seq);
	i=Rebuild::build(1,seq.size(),seq);
}

其他基本操作

插入

插入(Insert)一个元素,与二叉搜索树类似,不过需要在递归完后上推数据,还需要判断是否失衡,如果失衡,那么就重构。

代码:

void newnode(int &i,int v){// 新建节点
	i=(++tot);
	if(!root)root=i;
	t[i].init(v);
}

void insert(int &i,int v){
	if(!i){
		newnode(i,v);
		return;
	}
	if(t[i].w==v)t[i].cnt++;
	else if(t[i].w>v)insert(ls,v);
	else insert(rs,v);
	pushup(i);
	if(need_rebuild(i))rebuild(i);
}

删除

删除(Remove)一个元素,我们使用其他大多数平衡树使用的【懒删除】策略。只是将沿途的 \(\texttt{sz}\) 减 \(1\)。如果到了要删除的节点,那么也要将 \(\texttt{cnt}\) 减 \(1\)。最后和插入一样,也需要上推数据和判断失衡。

代码:

void remove(int &i,int v){
	if(!i)return;
	t[i].sz--;
	if(t[i].w==v){
		if(t[i].cnt>0)t[i].cnt--;
		return;
	}
	if(t[i].w>v)remove(ls,v);
	else remove(rs,v);
	pushup(i);
	if(need_rebuild(i))rebuild(i);
}

查询排名,查询排名对应的元素,查前驱,查后继

这一部分和二叉搜索树一模一样。就不讲了,只贴代码:

int kth(int &i,int k){
	if(!i)return 0;
	if(t[ls].sz>=k)return kth(ls,k);
	if(t[ls].sz<k-t[i].cnt)return kth(rs,k-t[ls].sz-t[i].cnt);
	return t[i].w;
}

int rnk(int &i,int v){
	if(!i)return 1;
	if(t[i].w>v)return rnk(ls,v);
	if(t[i].w<v)return rnk(rs,v)+t[ls].sz+t[i].cnt;
	return t[ls].sz+1;
}

int upper_bound(int &i,int v,bool great=0){
	if(!i)return !great;
	if(t[i].w==v&&t[i].cnt>0)return t[ls].sz+(!great)*(t[i].cnt+1);
	if(!great){
		if(v<t[i].w)return upper_bound(ls,v);
		return upper_bound(rs,v)+t[ls].sz+t[i].cnt;
	}
	if(t[i].w<v)return upper_bound(rs,v,1)+t[ls].sz+t[i].cnt;
	return upper_bound(ls,v,1);
}

int pre(int &i,int v){
	return kth(i,upper_bound(i,v,1));
}

int next(int &i,int v){
	return kth(i,upper_bound(i,v));
}

P6136 【模板】普通平衡树(数据加强版)

模板题,代码如下:

显示代码
#include <bits/stdc++.h>
#define int long long
using namespace std;

namespace ScapegoatTree{
#define ls (t[i].l)
#define rs (t[i].r)
	struct node{
		int s,sz,sd,cnt,l,r,w;
		void init(int weight){
			w=weight;
			l=r=0;
			s=sz=sd=cnt=1;
		}
	} t[10000005];
	int root,tot;
	const double alpha=0.7;
	
	void pushup(int i){
		t[i].s=t[ls].s+t[rs].s+1;
		t[i].sz=t[ls].sz+t[rs].sz+t[i].cnt;
		t[i].sd=t[ls].sd+t[rs].sd+(t[i].cnt>0);
	}
	
	bool need_rebuild(int i){
		if(t[i].cnt==0)return false;
		if(alpha*t[i].s<=(double)(max(t[ls].s,t[rs].s)))return true;
		if((double)t[i].sd<=alpha*t[i].s)return true;
		return false;
	}
	
	namespace Rebuild{
		void flatten(int i,vector<int> &seq){
			if(!i)return;
			flatten(ls,seq);
			if(t[i].cnt)seq.push_back(i);
			flatten(rs,seq);
		}
		int build(int l,int r,const vector<int> &seq){
			if(l==r)return 0;
			int mid=(l+r)>>1;
			int i=seq[mid];
			ls=build(l,mid,seq);
			rs=build(mid+1,r,seq);
			pushup(i);
			return i;
		}
	}
	
	void rebuild(int &i){
		vector<int> seq;
		seq.push_back(0);
		Rebuild::flatten(i,seq);
		i=Rebuild::build(1,seq.size(),seq);
	}
	
	void newnode(int &i,int v){
		i=(++tot);
		if(!root)root=i;
		t[i].init(v);
	}
	
	void insert(int &i,int v){
		if(!i){
			newnode(i,v);
			return;
		}
		if(t[i].w==v)t[i].cnt++;
		else if(t[i].w>v)insert(ls,v);
		else insert(rs,v);
		pushup(i);
		if(need_rebuild(i))rebuild(i);
	}
	
	void remove(int &i,int v){
		if(!i)return;
		t[i].sz--;
		if(t[i].w==v){
			if(t[i].cnt>0)t[i].cnt--;
			return;
		}
		if(t[i].w>v)remove(ls,v);
		else remove(rs,v);
		pushup(i);
		if(need_rebuild(i))rebuild(i);
	}
	
	int kth(int &i,int k){
		if(!i)return 0;
		if(t[ls].sz>=k)return kth(ls,k);
		if(t[ls].sz<k-t[i].cnt)return kth(rs,k-t[ls].sz-t[i].cnt);
		return t[i].w;
	}
	
	int rnk(int &i,int v){
		if(!i)return 1;
		if(t[i].w>v)return rnk(ls,v);
		if(t[i].w<v)return rnk(rs,v)+t[ls].sz+t[i].cnt;
		return t[ls].sz+1;
	}
	
	int upper_bound(int &i,int v,bool great=0){
		if(!i)return !great;
		if(t[i].w==v&&t[i].cnt>0)return t[ls].sz+(!great)*(t[i].cnt+1);
		if(!great){
			if(v<t[i].w)return upper_bound(ls,v);
			return upper_bound(rs,v)+t[ls].sz+t[i].cnt;
		}
		if(t[i].w<v)return upper_bound(rs,v,1)+t[ls].sz+t[i].cnt;
		return upper_bound(ls,v,1);
	}
	
	int pre(int &i,int v){
		return kth(i,upper_bound(i,v,1));
	}
	
	int next(int &i,int v){
		return kth(i,upper_bound(i,v));
	}
}

int last=0,ans=0;

signed main(){
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	int m,n;cin>>m>>n;
	while(m--){
		int v;cin>>v;ScapegoatTree::insert(ScapegoatTree::root,v);
	}
	while(n--){
		int op,x;
		cin>>op>>x;
		x^=last;
		if(op==1)ScapegoatTree::insert(ScapegoatTree::root,x);
		if(op==2)ScapegoatTree::remove(ScapegoatTree::root,x);
		if(op==3)last=ScapegoatTree::rnk(ScapegoatTree::root,x);
		if(op==4)last=ScapegoatTree::kth(ScapegoatTree::root,x);
		if(op==5)last=ScapegoatTree::pre(ScapegoatTree::root,x);
		if(op==6)last=ScapegoatTree::next(ScapegoatTree::root,x);
		if(op==3||op==4||op==5||op==6){
			ans^=last;
		}
	}
	cout<<ans;
	return 0;
}

标签:sz,cnt,return,seq,int,替罪羊,笔记,学习,ls
From: https://www.cnblogs.com/zheyuanxie/p/scapegoat-tree.html

相关文章

  • 强化学习
    目录​第一讲:什么是强化学习​机械学习的分类​强化学习的过程​强化学习的理论基础​第一讲:什么是强化学习​1.机械学习的分类​机器学习占据人工智能领域研究中很大的一部......
  • html学习笔记二 表单标签
    <!DOCTYPEhtml><htmllang="en"><head>  <metacharset="UTF-8">  <metahttp-equiv="X-UA-Compatible"content="IE=edge">  <metaname="viewport"content=......
  • 【Docker学习教程系列】8-如何将本地的Docker镜像发布到私服?
    通过前面的学习,我们已经知道,怎么将本地自己制作的镜像发布到阿里云远程镜像仓库中去。但是在实际工作开发中,一般,我们都是将公司的镜像发布到公司自己搭建的私服镜像仓库中,那......
  • for循环的使用与学习
    在Linux中使用BashFor循环你会嘛!原创 入门小站 入门小站 2022-12-0921:50 发表于湖北收录于合集#Linux645个入门小站分享运维技巧及10k+Stars的开......
  • 人工智能与机器学习
    OverridetheentrypointofanimageIntroducedinGitLabandGitLabRunner9.4.Readmoreaboutthe extendedconfigurationoptions.Beforeexplainingtheav......
  • 【Docker学习教程系列】8-如何将本地的Docker镜像发布到私服?
    通过前面的学习,我们已经知道,怎么将本地自己制作的镜像发布到阿里云远程镜像仓库中去。但是在实际工作开发中,一般,我们都是将公司的镜像发布到公司自己搭建的私服镜像仓库中,......
  • PyTorch中学习率调度器可视化介绍
    神经网络有许多影响模型性能的超参数。一个最基本的超参数是学习率(LR),它决定了在训练步骤之间模型权重的变化程度。在最简单的情况下,LR值是0到1之间的固定值。选择正确的......
  • 算法学习笔记(37)——扩展欧几里得算法
    扩展欧几里得算法扩展欧几里得算法欧几里得算法/辗转相除法(Euclideanalgorithm)裴蜀定理(Bézout定理)扩展欧几里得算法(ExtendedEuclideanalgorithm)求解线性同余方......
  • 算法学习笔记(36)——快速幂
    快速幂快速幂快速幂快速幂求逆元快速幂用于快速(在\(O(\logk)\)的时间复杂度之内)求出\(a^k\bmodp\)的结果,\(1\lea,p,k\le10^9\),核心是反复平方法。算......
  • 算法学习笔记(35)——欧拉函数
    欧拉函数欧拉函数用公式求欧拉函数用筛法求欧拉函数欧拉函数:在数论中,对正整数\(N\),欧拉函数\(\varphi(N)\)是小于等于\(N\)的正整数中与\(N\)互质的数的......