RuntimeErr
2021-06-27 23:49:00
van Emde Boas 树(以下简称 vEB 树),是由荷兰计算机科学家
当我翻开 《算法导论》 的目录的时候,一下子就被这个奇特的名字吸引住了。在学习过程中,我发现这个数据结构构思极其巧妙。因此,我将按照原文的思路,采用更通俗易懂的表述介绍这个数据结构,这篇文章也是我的学习笔记。此外,网上有关 vEB 树的实现均为指针实现,我这里将会给出无指针的实现。
最后,这篇文章如果有什么疏漏或错误之处,还请读者指出。这篇文章仅作抛砖引玉啦~
一定的树数据结构知识。
master 定理(分析时间复杂度的时候用)。
我们知道,像红黑树,二叉堆等支持优先队列操作的数据结构,它们的某些重要操作的时间复杂度的最坏(或摊还)情况的时间复杂度需
在这篇文章里,我们会看到 vEB 树也支持优先队列和一些其他操作,分别是:查询元素是否存在、插入和删除元素、查询前驱和后继、查询集合里的最大值和最小值,且每个操作的时间复杂度都只需惊人的
为避免歧义,以后我们用
由于我们只存储关键字的值域,可以开一个桶。若元素
前面讲到,如果单开一个桶,查询前驱和后继扫描的时间将会非常久。那么,如何缩短扫描的时间呢?
我们考虑在桶上叠加一棵二叉树,如图:
其中每一个非叶结点都代表一段值域的元素,其值为两个儿子的逻辑或,表示其代表的值域是否存在元素。我们来看看如何实现操作:
查询最大值:从根节点出发,一直走最右边的非零结点。
查询最小值:从根节点出发,一直走最左边的非零结点。
查询某个元素的前驱:从该元素出发,一直向上走,直到从右边进入某个结点,且该结点的左儿子非零,查询以该左儿子为根的最大值。(图中即为查询
查询某个元素的后继:从该元素出发,一直向上走,直到从左边进入某个结点,且该结点的右儿子非零,查询以该右儿子为根的最小值。
插入某个元素:从根节点(或该元素)出发,把从根节点到该元素的路径上的每个结点都赋值为
删除某个元素:将该元素的值置为
我们会发现,除了查询一个元素是否存在(直接查桶
旁白:???
别着急,慢慢来嘛~
有的时候,我们不一定要二叉树,我们可以像 B 树一样,使用多叉的结构。这样,整棵树的高度就减小了许多。
我们令值域
同样地,每一个非叶结点都代表一段值域的元素,其值为所有儿子的逻辑或。我们可以把这些非叶节点定义成一个数组
我们来考虑一下如何实现一些操作:
查询最大值:在
查询最小值:在
查询某个元素的前驱:在该元素所在簇中向左找,如果有
查询某个元素的后继:在该元素所在簇中向右找,如果有
插入一个元素:置该元素和该元素所在簇为
删除一个元素:置该元素为
我们可以发现,除了插入和查询某个元素是否存在(都是
旁白:???
虽然慢,但这个思想为后续 vEB 树的讲解奠定了重要的基础。
本节中假设
另外,从这节开始,有操作的部分我将会附上代码。
这里一一来解释一下:为什么是
我们考虑一种递归结构,像上面的大小为
像这样的的结构 我们定义它为原型 vEB 结构(proto_vEB)。(至于为什么是原型是因为它与真正的 vEB 结构还有区别)
下图就是一个原型 vEB 结构:
其中
包含
一个
结构体以及定义的实现:
struct proto_vEB{
int u;
bool A[2];//基本结构
int summary;
vector<int>cluster;
//用int是因为我之后建树将使用无指针
//vector是因为怕炸空间(
}tre[MAXN];
inline int high(int p,int x){
int u=(int)sqrt(tre[p].u);
return x/u;
}
inline int low(int p,int x){
int u=(int)sqrt(tre[p].u);
return x%u;
}
inline int index(int p,int x,int y){
int u=(int)sqrt(tre[p].u);
return x*u+y;
}
我们把这些结构组合起来,就形成了一个原型 vEB 树:
是不是很眼花缭乱?为了方便说明,我给每一个结构都编了序号(注意编号和序号的区分)。
这个
这里我还要补充一下上面
讲到这里可能会有点乱,建议反复看几次加强理解。
在讲操作之前,我要给出两个递归式,并根据《算法导论》给出解法,这两个递归式是后续操作计算时间复杂度时的两种情况:
考虑变量替换法,令
重命名
根据 master 定理解得
同样考虑变量替换法,令
重命名
根据 master 定理解得
好啦,我们可以开始操作讲解啦!
bool PROTO_vEB_MEMBER(int p,int x){
if(tre[p].u==2)return tre[p].A[x];
return PROTO_vEB_MEMBER(tre[p].cluster[high(p,x)],low(p,x))
}
这个很简单,如果是基本结构直接返回
void PROTO_vEB_INSERT(int p,int x){
if(tre[p].u==2)tre[p].A[x]=1;
else {
PROTO_vEB_INSERT(tre[p].cluster[high(p,x)],low(p,x));
PROTO_vEB_INSERT(tre[p].summary,high(p,x));
}
}
这个也很简单,如果是基本结构直接置
这个相比于插入要复杂的多,我们需要判断簇中还有没有元素,可以添加属性 可恶不能当懒人了)。相应地,我们的插入也要修改一下。
int PROTO_vEB_INSERT(int p,int x){//不保证x一定在集合内
if(tre[p].u==2){
if(!tre[p].A[x]){
++tre[p].num;
tre[p].A[x]=true;
return 1;
}
return 0;
}
int tmp1=PROTO_vEB_INSERT(tre[p].cluster[high(p,x)],low(p,x)),tmp2=PROTO_vEB_INSERT(tre[p].summary,high(p,x));
tre[tre[p].cluster[high(p,x)]].num+=tmp1;
tre[tre[p].summary].num+=tmp2;
return tmp1+tmp2;
}
int PROTO_vEB_DELETE(int p,int x){//保证x一定在集合内
if(tre[p].u==2){
--tre[p].num;tre[p].A[x]=0;
return 1;
}
int tmp1=PROTO_vEB_DELETE(tre[p].cluster[high(p,x)],low(x)),tmp2=0;
tre[p].num-=tmp1;
if(!tre[p].num){
tmp2=PROTO_vEB_DELETE(tre[p].summary,high(p,x));
tre[tre[p].summary].num-=tmp2;
}
return tmp1+tmp2;
}
我们发现,两个函数的返回值都是int
,它们分别表示什么意思呢?
插入中的返回值表示的是新插入的元素的个数,同样,删除中的返回值表示的就是删除的元素的个数。
插入的部分很容易懂我就不讲了,看看删除。如果是基本结构,删除了数之后直接返回
考虑最坏情况,删除的过程要调用两次函数,可以用递归式二表示,时间复杂度
这里的查询最小值表示的是查询某个结构中的最小值。
int PROTO_vEB_MINIMUM(int p){
if(tre[p].u==2){
if(tre[p].A[0])return 0;
if(tre[p].A[1])return 1;
return NIL;
}
int min_cluster=PROTO_vEB_MINIMUM(tre[p].summary);
if(min_cluster==NIL)return NIL;
int offset=PROTO_vEB_MINIMUM(tre[p].cluster[min_cluster]);
return index(min_cluster,offset);
}
如果是基本结构就返回里面最小的,如果这个基础结构不包含任何元素则返回
这个操作需要调用两次递归,为递归式二,时间复杂度
由于查询最大值操作是对称的,这里就不赘述。
int PROTO_vEB_SUCCESSOR(int p,int x){
if(tre[p].u==2){
if(x==0&&tre[p].A[1])return 1;
return NIL;
}
int offset=PROTO_vEB_SUCCESSOR(tre[p].cluster[high(p,x)],low(p,x));
if(offset!=NIL)return index(p,high(p,x),offset);
int succ_cluster=PROTO_vEB_SUCCESSOR(tre[p].summary,high(p,x));
if(succ_cluster==NIL)return NIL;
offset=PROTO_vEB_MINIMUM(tre[p].cluster[succ_cluster])return index(p,succ_cluster,offset);
}
如果是基本结构,判断查询的值是不是前一个元素且后一个元素存在,否则返回
这里的时间复杂度比较特殊,我们需要调用两次查询后继的函数和一次最小值函数,可以列出这样的递归式:
还是变量替换法,令
重命名
根据 master 定理解得
查询前驱操作也是对称的,这里也不赘述。
至此,原型 vEB 树的部分已经讲完。虽然原型 vEB 树的效率已经很好,但是还没全部达到预期的
还记得第
这里我们记 这 LaTex 有够难打的),即
vEB 的结构是以 proto_vEB 为基础修改而来,我们称一个大小为
除此之外,我们还新增了两个属性:
如图就是一个 vEB 结构:
结构体和定义:
map<int,int>lg;
struct vEB{
int u,summary;
vector<int>cluster;
int min,max;
}vEB[MAXN];
void init(){
for(int i=0;i<32;++i)lg[1<<i]=i;//预处理log
}
inline int high(int p,int x){
int u=1<<(lg[vEB[p].u]>>1);
return x/u;
}
inline int low(int p,int x){
int u=1<<(lg[vEB[p].u]>>1);
return x%u;
}
inline int index(int p,int x,int y){
int u=1<<(lg[vEB[p].u]>>1);
return x*u+y;
}
特别地,我们有一些性质:
如果是基础结构,那么我们不再需要用
通过
以下就是一棵标准的 vEB,其表示的集合跟之前 proto_vEB 的图一样,上述的性质均可在里面体现。
下面我还是要给出一个递归式,根据《算法导论》给出解法,对于 vEB 的所有递归过程的操作的时间复杂度,我们都可以用下面的式子表示。
令
注意到,对于所有的
重命名
根据 master 定理,有解
bool Tree_Member(int p,int x){
if(x==vEB[p].min||x==vEB[p].max)return true;
if(vEB[p].u==2)return false;
return Tree_Member(vEB[p].cluster[high(p,x)],low(p,x));
}
第一句话就是缩短运行时间的,如果
inline int Tree_Minimum(int p){
return vEB[p].min;
}
inline int Tree_Maximum(int p){
return vEB[p].max;
}
这个不用说了吧,
int Tree_Successor(int p,int x){
if(vEB[p].u==2){
if(x==0&&vEB[p].max==1)return 1;
return NIL;
}
if(vEB[p].min!=NIL&&x<vEB[p].min)return vEB[p].min;
int offset,max_low=Tree_Maximum(vEB[p].cluster[high(p,x)]);
if(max_low!=NIL&&low(p,x)<max_low){
offset=Tree_Successor(vEB[p].cluster[high(p,x)],low(p,x));
return index(p,high(p,x),offset);
}
int succ_cluster=Tree_Successor(vEB[p].summary,high(p,x));
if(succ_cluster==NIL)return NIL;
offset=Tree_Minimum(vEB[p].cluster[succ_cluster]);
return index(p,succ_cluster,offset);
}
判断基础结构的语句和原型一样。注意到,如果
整个过程最多只会调用一次递归,原先查询簇内是否有后继和查询后继簇的最小值的
为什么前驱不和后继一起讲呢?因为前驱函数大体和后继函数是对称的,但是多加了一个判断:
int Tree_Predecessor(int p,int x){
if(vEB[p].u==2){
if(x==1&&vEB[p].min==0)return 0;
return NIL;
}
if(vEB[p].max!=NIL&&x>vEB[p].max)return vEB[p].max;
int offset,min_low=Tree_Minimum(vEB[p].cluster[high(p,x)]);
if(min_low!=NIL&&low(p,x)>min_low){
offset=Tree_Predecessor(vEB[p].cluster[high(p,x)],low(p,x));
return index(p,high(p,x),offset);
}
int pred_cluster=Tree_Predecessor(vEB[p].summary,high(p,x));
if(pred_cluster==NIL){
if(vEB[p].min!=NIL&&x>vEB[p].min)return vEB[p].min;
return NIL;
}
offset=Tree_Maximum(vEB[p].cluster[pred_cluster]);
return index(p,pred_cluster,offset);
}
这一句判断出现在第
if(vEB[p].min!=NIL&&x>vEB[p].min)return vEB[p].min;
还记得之前说到的性质吗,
在原型 vEB 里,插入需要两次递归:往簇里插入元素,将簇插入到
答案很简单,当该簇中有元素时,说明该簇已经存在于
inline void Empty_Tree_Insert(int p,int x){
vEB[p].max=vEB[p].min=x;
}
void Tree_Insert(int p,int x){
if(vEB[p].min==NIL){
Empty_Tree_Insert(p,x);
return;
}
if(x<vEB[p].min)swap(x,vEB[p].min);
if(vEB[p].u>2){
if(Tree_Minimum(vEB[p].cluster[high(p,x)])==NIL){
Tree_Insert(vEB[p].summary,high(p,x));
Empty_Tree_Insert(vEB[p].cluster[high(p,x)],low(p,x));
}else Tree_Insert(vEB[p].cluster[high(p,x)],low(p,x));
}
if(x>vEB[p].max)vEB[p].max=x;
return;
}
如果
所有操作里面最难理解的来了哦。
void Tree_Delete(int p,int x){//保证x存在于集合中
if(vEB[p].min==vEB[p].max){
vEB[p].min=vEB[p].max=NIL;
return;
}
if(vEB[p].u==2){
vEB[p].max=vEB[p].min=!x;
return;
}
if(x==vEB[p].min){
int first_cluster=Tree_Minimum(vEB[p].summary);
x=index(p,first_cluster,Tree_Minimum(vEB[p].cluster[first_cluster]));
vEB[p].min=x;
}
Tree_Delete(vEB[p].cluster[high(p,x)],low(p,x));
if(Tree_Minimum(vEB[p].cluster[high(p,x)])==NIL){
Tree_Delete(vEB[p].summary,high(p,x));
if(x==vEB[p].max){
int summary_max=Tree_Maximum(vEB[p].summary);
if(summary_max==NIL)vEB[p].max=vEB[p].min;//如果summary里面为空,那么剩下的就只有min了(就算min的值为NIL也直接赋值给max)
else vEB[p].max=index(p,summary_max,Tree_Maximum(vEB[p].cluster[summary_max]));//否则重新设置max
}
}else if(x==vEB[p].max)vEB[p].max=index(p,high(p,x),Tree_Maximum(vEB[p].cluster[high(p,x)]));
return;
}
首先确定结构内元素的个数,如果只有一个,像第一句判断一样,直接就删除了,否则结构内至少有两个元素。如果是基础结构(两个元素),那么置
首先考虑如果删除的是
然后我们递归删除簇内的这个元素,如果删掉后簇为空了,那么就从
如果删掉
乍一看,删除函数里面最多会调用两次递归,但是注意调用的条件:我们调用第二次递归的前提是
第一次调用只用
第二次调用不会发生。
所以删除操作的时间复杂度还是
终于讲完啦!
普通van Emde Boas树
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e6,NIL=-1;
int cnt;
map<int,int>lg;
struct vEB{
int u,summary;
vector<int>cluster;
int min,max;
}vEB[MAXN];
inline void Tree_Build(int p,int size){//u=2^size
vEB[p].summary=vEB[p].min=vEB[p].max=NIL;
if(size<=1){
vEB[p].u=2;return;
}
vEB[p].u=1<<size;
int cluster_size=(size>>1)+(size&1);//即ceil(log(u)/2)
vEB[p].summary=++cnt;
Tree_Build(vEB[p].summary,cluster_size);
vEB[p].cluster.resize(1<<cluster_size); //节省空间用的
for(int i=0;i<1<<cluster_size;++i){
vEB[p].cluster[i]=++cnt;
Tree_Build(vEB[p].cluster[i],size>>1);
}
return;
}
void init(){
for(int i=0;i<32;++i)lg[1<<i]=i;
}
inline int high(int p,int x){
int u=1<<(lg[vEB[p].u]>>1);
return x/u;
}
inline int low(int p,int x){
int u=1<<(lg[vEB[p].u]>>1);
return x%u;
}
inline int index(int p,int x,int y){
int u=1<<(lg[vEB[p].u]>>1);
return x*u+y;
}
inline int Tree_Minimum(int p){
return vEB[p].min;
}
inline int Tree_Maximum(int p){
return vEB[p].max;
}
bool Tree_Member(int p,int x){
if(x==vEB[p].min||x==vEB[p].max)return true;
if(vEB[p].u==2)return false;
return Tree_Member(vEB[p].cluster[high(p,x)],low(p,x));
}
int Tree_Successor(int p,int x){
if(vEB[p].u==2){
if(x==0&&vEB[p].max==1)return 1;
return NIL;
}
if(vEB[p].min!=NIL&&x<vEB[p].min)return vEB[p].min;
int offset,max_low=Tree_Maximum(vEB[p].cluster[high(p,x)]);
if(max_low!=NIL&&low(p,x)<max_low){
offset=Tree_Successor(vEB[p].cluster[high(p,x)],low(p,x));
return index(p,high(p,x),offset);
}
int succ_cluster=Tree_Successor(vEB[p].summary,high(p,x));
if(succ_cluster==NIL)return NIL;
offset=Tree_Minimum(vEB[p].cluster[succ_cluster]);
return index(p,succ_cluster,offset);
}
int Tree_Predecessor(int p,int x){
if(vEB[p].u==2){
if(x==1&&vEB[p].min==0)return 0;
return NIL;
}
if(vEB[p].max!=NIL&&x>vEB[p].max)return vEB[p].max;
int offset,min_low=Tree_Minimum(vEB[p].cluster[high(p,x)]);
if(min_low!=NIL&&low(p,x)>min_low){
offset=Tree_Predecessor(vEB[p].cluster[high(p,x)],low(p,x));
return index(p,high(p,x),offset);
}
int pred_cluster=Tree_Predecessor(vEB[p].summary,high(p,x));
if(pred_cluster==NIL){
if(vEB[p].min!=NIL&&x>vEB[p].min)return vEB[p].min;
return NIL;
}
offset=Tree_Maximum(vEB[p].cluster[pred_cluster]);
return index(p,pred_cluster,offset);
}
inline void Empty_Tree_Insert(int p,int x){
vEB[p].max=vEB[p].min=x;
}
void Tree_Insert(int p,int x){
if(vEB[p].min==NIL){
Empty_Tree_Insert(p,x);
return;
}
if(x<vEB[p].min)swap(x,vEB[p].min);
if(vEB[p].u>2){
if(Tree_Minimum(vEB[p].cluster[high(p,x)])==NIL){
Tree_Insert(vEB[p].summary,high(p,x));
Empty_Tree_Insert(vEB[p].cluster[high(p,x)],low(p,x));
}else Tree_Insert(vEB[p].cluster[high(p,x)],low(p,x));
}
if(x>vEB[p].max)vEB[p].max=x;
return;
}
void Tree_Delete(int p,int x){
if(vEB[p].min==vEB[p].max){
vEB[p].min=vEB[p].max=NIL;
return;
}
if(vEB[p].u==2){
vEB[p].max=vEB[p].min=!x;
return;
}
if(x==vEB[p].min){
int first_cluster=Tree_Minimum(vEB[p].summary);
x=index(p,first_cluster,Tree_Minimum(vEB[p].cluster[first_cluster]));
vEB[p].min=x;
}
Tree_Delete(vEB[p].cluster[high(p,x)],low(p,x));
if(Tree_Minimum(vEB[p].cluster[high(p,x)])==NIL){
Tree_Delete(vEB[p].summary,high(p,x));
if(x==vEB[p].max){
int summary_max=Tree_Maximum(vEB[p].summary);
if(summary_max==NIL)vEB[p].max=vEB[p].min;
else vEB[p].max=index(p,summary_max,Tree_Maximum(vEB[p].cluster[summary_max]));
}
}else if(x==vEB[p].max)vEB[p].max=index(p,high(p,x),Tree_Maximum(vEB[p].cluster[high(p,x)]));
return;
}
int n,m,rt;
int main(){
init();
// freopen("std.in","r",stdin);
// freopen("my.out","w",stdout);
scanf("%d%d",&n,&m);
int u=0;for(--n;n;n>>=1,++u);
Tree_Build(rt,u);
while(m--){
int op,x;scanf("%d",&op);
if(op==1){
scanf("%d",&x);
if(!Tree_Member(rt,x))Tree_Insert(rt,x);
}else if(op==2){
scanf("%d",&x);
if(Tree_Member(rt,x))Tree_Delete(rt,x);
}else if(op==3){
printf("%d\n",Tree_Minimum(rt));
}else if(op==4){
printf("%d\n",Tree_Maximum(rt));
}else if(op==5){
scanf("%d",&x);
printf("%d\n",Tree_Predecessor(rt,x));
}else if(op==6){
scanf("%d",&x);
printf("%d\n",Tree_Successor(rt,x));
}else if(op==7){
scanf("%d",&x);
printf("%d\n",Tree_Member(rt,x)?1:-1);
}
}
return 0;
}
终于写完啦!!1,现在是 2021 年 6 月 27 日 23 时 39 分。
写这篇文章的原因不只是因为从《算法导论》里看到的,还有一个原因是之前在日报里看到过:Ynoi的学习笔记,只是很简单的讲了一下,但也激发了我很大的兴趣。vEB 树虽然实用性不强,但是能够很好的激发思维,这才是我写完这篇文章的动力。
谢谢观看。