van Emde Boas 树 学习笔记

概述

一种数据结构科技,可在 \(O(\log\log V)\) 的复杂度内支持插入、删除、查询前驱后继、查询元素是否存在、查询最值(相当于阉割了有关排名操作的平衡树),其中 \(V\) 是值域。

定义

首先把值域补到 \(2\) 的整数幂,设 \(V=2^u\)

维护一个桶表示元素是否存在。运用分块思想,将序列分为约 \(\sqrt V\) 块。为了保证分块后每一块还是 \(2\) 的整数幂,块长为 \(2^{\lfloor\frac u 2\rfloor}\),块数为 \(2^{\lceil\frac{u+1}2\rceil}\)

此时这个结构可以看作一颗高 \(2\) 的树。其中,每一个叶节点都是规模更小的子结构,考虑继续对每一块分块,直到 \(u=1\),这就形成了一颗 \(\sqrt V\) 叉树。

对于每个节点,维护块大小、块里的最大值和最小值、儿子的号。此外还需要一个数组 summary 来维护每个儿子里是否有数,发现这也是一个桶,所以也用一个类似的结构维护。

struct node{
  int u,sum,minn,maxn;
  vector<int>son;
};

另外有一个性质,如果一个数成为了某个节点的最小值,就不在子树里处理它,这可以保证复杂度。叶节点比较特殊,只需要最小和最大值的信息。

辅助函数

int high(int u,int k){
  return k>>(u>>1);
}
int low(int u,int k){
  return k&((1<<(u>>1))-1);
}
int index(int u,int x,int y){
  return x<<(u>>1)|y;
}

high 表示 \(k\)\(pos\) 的第几个子节点;

low 表示 \(k\)\(pos\) 的子节点的第几个;

index 则为第 \(x\) 个子节点的第 \(y\) 个在 \(pos\) 里是第几个。

建树

直接按照定义建出树。可以证明树的节点数为 \(O(V)\) 级别,建树的复杂度和空间复杂度也为 \(O(V)\)

void build(int pos,int u){
  tr[pos].minn=tr[pos].maxn=-1,tr[pos].u=u;
  if(u==1)return;
  tr[pos].sum=++cnt,build(cnt,(u+1)>>1);
  for(int i=0;i<1<<((u+1)>>1);i++)tr[pos].son.push_back(++cnt),build(cnt,u>>1);
}

查询最值

直接返回,\(O(1)\)

int front(int pos){
  return tr[pos].minn;
}
int back(int pos){
  return tr[pos].maxn;
}

查询元素是否存在

边界条件:如果元素等于最小或最大值,直接返回 \(1\)。否则,如果处于叶节点,那么元素不存在。

然后递归查询。

bool find(int pos,int k){
  if(tr[pos].minn==k||tr[pos].maxn==k)return 1;
  if(!tr[pos].u)return 0;
  return find(tr[pos].son[high(pos,k)],low(pos,k));
}

复杂度证明:

首先,vEB 树的高为 \(\log\log V\)。可以这么想,\(u=\log V\),而 \(u\) 每一层都会减半。

而这个操作只会递归一次,相当于从根节点走到叶节点,复杂度 \(O(\log\log V)\)。其他操作如果只递归一次,复杂度也一样。

询问前驱后继

先讲后继。如果处于叶节点,可以 \(O(1)\) 判断。

如果节点里所有元素都比查询值大,返回最小值。

然后分两种情况:如果答案就在查询值所在的子节点内(也就是查询值小于这个子节点的最大值),递归进这个子节点查询。否则在 summary 中查询这个子节点后第一个有值的块,答案为这个块的最小值。

int next(int pos,int k){
  if(!tr[pos].u)return k==0&&tr[pos].maxn==1?1:-1;
  if(tr[pos].minn!=-1&&tr[pos].minn>k)return tr[pos].minn;
  int temp=back(tr[pos].son[high(pos,k)]);
  if(temp!=-1&&low(pos,k)<temp)return index(pos,high(pos,k),next(tr[pos].son[high(pos,k)],low(pos,k)));
  temp=next(tr[pos].sum,high(pos,k));
  return temp==-1?-1:index(pos,temp,front(tr[pos].son[temp]));
}

这是一个分支结构,只会递归一次。

前驱与后继大致对称。只是多了一行:如果查询不到上一个有值的块,也有可能前驱是最小值。

int prev(int pos,int k){
  if(!tr[pos].u)return k==1&&tr[pos].minn==0?0:-1;
  if(tr[pos].maxn!=-1&&tr[pos].maxn<k)return tr[pos].maxn;
  int temp=front(tr[pos].son[high(pos,k)]);
  if(temp!=-1&&low(pos,k)>temp)return index(pos,high(pos,k),prev(tr[pos].son[high(pos,k)],low(pos,k)));
  temp=prev(tr[pos].sum,high(pos,k));
  return temp==-1?(tr[pos].minn!=-1&&tr[pos].minn<k?tr[pos].minn:-1):index(pos,temp,back(tr[pos].son[temp]));
}

插入

若节点没有数,更新最大和最小值后返回。

如果插入的数比最小值小,此时插入的数不用继续递归,而原来的最小值需要进入到子树中,交换一下即可。

如果插入的值所属的子节点没有数,就需要更新 summary。然后递归进子树。最后更新最大值。

void insert(int pos,int k){
  if(tr[pos].minn==-1){
    tr[pos].minn=tr[pos].maxn=k;
    return;
  }
  if(tr[pos].minn>k)swap(tr[pos].minn,k);
  if(tr[pos].u){
    if(front(tr[pos].son[high(pos,k)])==-1)insert(tr[pos].sum,high(pos,k));
    insert(tr[pos].son[high(pos,k)],low(pos,k));
  }
  if(tr[pos].maxn<k)tr[pos].maxn=k;
}

这里看似可能递归两次,其实如果执行了第一次递归,说明这个子节点没有数,第二次递归就会 \(O(1)\) 判掉。

删除

首先判掉只有一个数和叶节点的情况。

如果删除的是最小值,要把删除的值和最小值都更新为次小值,即最小的有值的子节点的最小值。

然后在子树里删除。如果删除后这个子节点空了,就在 summary 中删除。

更新最大值分两种情况:若删之后子节点空了,新的最大值不在这个节点内,赋值为最大的有值的子节点的最大值。如果查找不到则最大值和最小值一样。否则只需要在这个子节点里找。

复杂度分析同插入。

void erase(int pos,int k){
  if(tr[pos].minn==tr[pos].maxn){
    tr[pos].minn=tr[pos].maxn=-1;
    return;
  }
  if(!tr[pos].u){
    tr[pos].minn=tr[pos].maxn=!k;
    return;
  }
  int temp;
  if(tr[pos].minn==k)temp=front(tr[pos].sum),tr[pos].minn=k=index(pos,temp,front(tr[pos].son[temp]));
  erase(tr[pos].son[high(pos,k)],low(pos,k));
  if(front(tr[pos].son[high(pos,k)])==-1){
    erase(tr[pos].sum,high(pos,k));
    if(tr[pos].maxn==k){
      temp=back(tr[pos].sum);
      if(temp==-1)tr[pos].maxn=tr[pos].minn;
      else tr[pos].maxn=index(pos,temp,back(tr[pos].son[temp]));
    }
  }
  else if(tr[pos].maxn==k)tr[pos].maxn=index(pos,high(pos,k),back(tr[pos].son[high(pos,k)]));
}

优化

当前节点的大小不超过 \(64\) 时,可以不用继续分块而是直接用一个 unsigned long long 维护,减少了很多常数。

如果值域很大,可以对 vEB 树动态开点,这也被称为 RS-vEB 树。

struct node{
  int sum,minn,maxn;
  unsigned long long v;
  unordered_map<int,int>son;
  node(){
    minn=maxn=-1;
  }
};
template<int maxn>struct vEB{
  node tr[maxn];
  int cnt;
  int high(int u,int k){
    return k>>(u>>1);
  }
  int low(int u,int k){
    return k&((1<<(u>>1))-1);
  }
  int index(int u,int x,int y){
    return x<<(u>>1)|y;
  }
  int front(int pos,int u){
    return u<=6?(tr[pos].v?__builtin_ctzll(tr[pos].v):-1):tr[pos].minn;
  }
  int back(int pos,int u){
    return u<=6?(tr[pos].v?63-__builtin_clzll(tr[pos].v):-1):tr[pos].maxn;
  }
  bool find(int &pos,int u,int k){
    if(!pos)pos=++cnt;
    if(u<=6)return tr[pos].v>>k&1;
    if(tr[pos].minn==k||tr[pos].maxn==k)return 1;
    return find(tr[pos].son[high(u,k)],u>>1,low(u,k));
  }
  int prev(int &pos,int u,int k){
    if(!pos)pos=++cnt;
    if(u<=6)return tr[pos].v&((1ull<<k)-1)?63-__builtin_clzll(tr[pos].v&((1ull<<k)-1)):-1;
    if(tr[pos].maxn!=-1&&tr[pos].maxn<k)return tr[pos].maxn;
    int temp=front(tr[pos].son[high(u,k)],u>>1);
    if(temp!=-1&&low(u,k)>temp)return index(u,high(u,k),prev(tr[pos].son[high(u,k)],u>>1,low(u,k)));
    temp=prev(tr[pos].sum,(u+1)>>1,high(u,k));
    return temp==-1?(tr[pos].minn!=-1&&tr[pos].minn<k?tr[pos].minn:-1):index(u,temp,back(tr[pos].son[temp],u>>1));
  }
  int next(int &pos,int u,int k){
    if(!pos)pos=++cnt;
    if(u<=6)return tr[pos].v>>(k+1)?k+1+__builtin_ctzll(tr[pos].v>>(k+1)):-1;
    if(tr[pos].minn!=-1&&tr[pos].minn>k)return tr[pos].minn;
    int temp=back(tr[pos].son[high(u,k)],u>>1);
    if(temp!=-1&&low(u,k)<temp)return index(u,high(u,k),next(tr[pos].son[high(u,k)],u>>1,low(u,k)));
    temp=next(tr[pos].sum,(u+1)>>1,high(u,k));
    return temp==-1?-1:index(u,temp,front(tr[pos].son[temp],u>>1));
  }
  void insert(int &pos,int u,int k){
    if(!pos)pos=++cnt;
    if(u<=6){
      tr[pos].v|=1ull<<k;
      return;
    }
    if(tr[pos].minn==-1){
      tr[pos].minn=tr[pos].maxn=k;
      return;
    }
    if(tr[pos].minn>k)swap(tr[pos].minn,k);
    if(front(tr[pos].son[high(u,k)],u>>1)==-1)insert(tr[pos].sum,(u+1)>>1,high(u,k));
    insert(tr[pos].son[high(u,k)],u>>1,low(u,k));
    if(tr[pos].maxn<k)tr[pos].maxn=k;
  }
  void erase(int &pos,int u,int k){
    if(!pos)pos=++cnt;
    if(u<=6){
      tr[pos].v^=tr[pos].v&1ull<<k;
      return;
    }
    if(tr[pos].minn==tr[pos].maxn){
      tr[pos].minn=tr[pos].maxn=-1;
      return;
    }
    int temp;
    if(tr[pos].minn==k)temp=front(tr[pos].sum,(u+1)>>1),tr[pos].minn=k=index(u,temp,front(tr[pos].son[temp],u>>1));
    erase(tr[pos].son[high(u,k)],u>>1,low(u,k));
    if(front(tr[pos].son[high(u,k)],u>>1)==-1){
      erase(tr[pos].sum,(u+1)>>1,high(u,k));
      if(tr[pos].maxn==k){
        temp=back(tr[pos].sum,(u+1)>>1);
        if(temp==-1)tr[pos].maxn=tr[pos].minn;
        else tr[pos].maxn=index(u,temp,back(tr[pos].son[temp],u>>1));
      }
    }
    else if(tr[pos].maxn==k)tr[pos].maxn=index(u,high(u,k),back(tr[pos].son[high(u,k)],u>>1));
  }
};

[[数据结构]]

posted @ 2024-03-01 09:29  lgh_2009  阅读(115)  评论(0)    收藏  举报