前言
被邀请赛和区域赛的平衡树踢死了,回来学平衡树了 T^T......
一、原理
1.建立
跳表的节点结构和链表非常相似,不同在于其拥有多层指针。重点是,跳表在分配指针的时候,不是以固定的方式分配的,还是采用随机化的方式分配多层指针。方法是,首先每个节点都有一级指针,之后考虑使用随机函数每次生成一个 0 到 1 之间的数字。若数字小于 0.5 就生成一个新的指针,否则就停止生成的过程。其中为了防止出现运气很好一直在生成的情况,考虑设置一个最大层数 maxL,一般都取 logn 的规模。

首先,考虑设置一个值为负无穷的初始节点,其每一层的指针都是满的,初始均指向 NULL。之后在加入的时候,若当前数为 x,那么就从高层往低层考虑,每次找小于 x 的最右节点。

对于 x=5 的新节点 2,其随机出了两层指针。从上往下考虑,在上四层中,由于 5 只有两层,所以都不需要插入。而到了第二层时,此时由于 5 有第二层的指针,所以就将其插到第二层。之后去往第一层,还是一样操作,最终初始节点 1 的第一二层指针就都指向 2 号节点。

在 x=20 的 3 号节点来到时,还是从上往下考虑,那么在前四层还是不需要插入。当来到第二层时,此时发现 1 号节点的第二层指针指向的 2 号节点的权值为 5,小于当前的 20,那么就向右移动到 2 号节点。此时由于 3 号节点依然没有第二层指针,所以就不插入。之后就顺着 2 号节点往下来到第一层指针,此时就可以插入了。
当 x=7 的 4 号节点来到时,此时由于其有四层指针,那么就需要插入 1 号节点的第三四层指针。之后在第二层,发现 2 号节点的 5 小于 7,所以让 2 号节点的第二层指针指向 4 号节点。之后顺着 2 号节点往下,再让其第一层的指针指向 4 号节点,4 号节点的第一层指针指向原本的 3 号节点即可。对于 x=15 的 5 号节点也是类似,就是在第四层发现 4 号节点的 7 小于 15,那么就来到 4 号节点。之后往下来到后面几层时,都在这后面插入即可。
2.复杂度
复杂度方面,由于是随机化的方式分配层数,所以可以认为节点的层数和权值无关。那么也就是说这个往下的阶梯是很均匀的,不会出现高层全是较小数,第一层全是较大数,从而导致每次都要来到第一层再往右走很多。又因为第一层的节点个数是 n,第二层的个数是 n/2,第三层的个数是 n/4。在这样的结构下,又因为是从大往下考虑,那么当在高层走过了一个节点,必然在第一层就会跳过很多个节点。具体证明太难了,可以这么感性地理解()
3.查询
在查询时,还是每次跳到小于 x 的最右位置,这样一直干到第一层时,由于每个节点都会有第一层,所以此时的节点必然就是答案。大于 x 和等于 x 只需要在这个的基础上,往右多走一步或者两步即可。而对于删除,还是每次跳到小于 x 的最右位置,如果下一个是 x 就删除然后往下即可。
对于查询 x 的排名的问题,不难想到需要在高层跳的时候维护出跨过了第一层的几个节点信息。那么就可以对每个指针再多维护跳过几个节点的信息,之后就是像查小于 x 一样跳,返回个数即可。查排名是 x 的时候也可以类似这么搞,就是每次看能否往后跳,能就减少再考虑即可。
二、题目------普通平衡树
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
template<typename T>
struct Skip_List
{
private:
int MAXL;
vector<T>key;
vector<int>count;
vector<int>level;
vector<vector<int>>next;
vector<vector<int>>len;
int cnt;
mt19937 myrand;
int rand(){
return uniform_int_distribution<int>(1,10)(myrand);
}
int random(){
int ans=1;
while(rand()<=5)
{
ans++;
}
return min(ans,MAXL);
}
void addCount(int i,int h,int num){
while(next[i][h]!=0&&key[next[i][h]]<num){
i=next[i][h];
}
if(h==1){
count[next[i][h]]++;
}else{
addCount(i,h-1,num);
}
len[i][h]++;
}
//return from i until j is inserted, cross how many number
int addNode(int i,int h,int j){
int rightCnt=0;
while(next[i][h]!=0&&key[next[i][h]]<key[j]){
rightCnt+=len[i][h];
i=next[i][h];
}
if(h==1){
next[j][h]=next[i][h];
next[i][h]=j;
len[j][h]=count[next[j][h]];
len[i][h]=1;
return rightCnt;
}
int downCnt=addNode(i,h-1,j);
if(h>level[j]){
len[i][h]++;
}else{
next[j][h]=next[i][h];
next[i][h]=j;
len[j][h]=len[i][h]-downCnt;
len[i][h]=downCnt+1;
}
return downCnt+rightCnt;
}
void eraseCount(int i,int h,int num){
while(next[i][h]!=0&&key[next[i][h]]<num){
i=next[i][h];
}
if(h==1){
count[next[i][h]]--;
}else{
eraseCount(i,h-1,num);
}
len[i][h]--;
}
void eraseNode(int i,int h,int j){
if(h<1){
return ;
}
while(next[i][h]!=0&&key[next[i][h]]<key[j]){
i=next[i][h];
}
if(h>level[j]){
len[i][h]--;
}else{
next[i][h]=next[j][h];
len[i][h]+=len[j][h]-1;
}
eraseNode(i,h-1,j);
}
int small(int i,int h,int num){
int rightCnt=0;
while(next[i][h]!=0&&key[next[i][h]]<num){
rightCnt+=len[i][h];
i=next[i][h];
}
if(h==1){
return rightCnt;
}
return rightCnt+small(i,h-1,num);
}
int index(int i,int h,int x){
int rightCnt=0;
while(next[i][h]!=0&&rightCnt+len[i][h]<x){
rightCnt+=len[i][h];
i=next[i][h];
}
if(h==1){
return key[next[i][h]];
}
return index(i,h-1,x-rightCnt);
}
int pre(int i,int h,int num){
while(next[i][h]!=0&&key[next[i][h]]<num){
i=next[i][h];
}
if(h==1){
return key[i];
}
return pre(i,h-1,num);
}
int post(int i,int h,int num){
while(next[i][h]!=0&&key[next[i][h]]<num){
i=next[i][h];
}
if(h==1){
if(next[i][h]==0){
return INF;
}
if(key[next[i][h]]>num){
return key[next[i][h]];
}
i=next[i][h];
if(next[i][h]==0){
return INF;
}
return key[next[i][h]];
}
return post(i,h-1,num);
}
public:
Skip_List(int n):MAXL(__lg(n)),key(n),count(n),level(n),next(n,vector<int>(__lg(n)+1)),len(n,vector<int>(__lg(n)+1)),myrand(time(0)){
cnt=1;
key[cnt]=-INF;
level[cnt]=MAXL;
}
int find(int i,int h,int num){
while(next[i][h]!=0&&key[next[i][h]]<num){
i=next[i][h];
}
if(h==1){
if(next[i][h]!=0&&key[next[i][h]]==num){
return next[i][h];
}
return 0;
}
return find(i,h-1,num);
}
void insert(int num){
if(find(1,MAXL,num)!=0){
addCount(1,MAXL,num);
}else{
key[++cnt]=num;
count[cnt]=1;
level[cnt]=random();
addNode(1,MAXL,cnt);
}
}
void erase(int num){
int j=find(1,MAXL,num);
if(j!=0){
if(count[j]>1){
eraseCount(1,MAXL,num);
}else{
eraseNode(1,MAXL,j);
}
}
}
int rank(int num){
return small(1,MAXL,num)+1;
}
int index(int x){
return index(1,MAXL,x);
}
int pre(int num){
return pre(1,MAXL,num);
}
int post(int num){
return post(1,MAXL,num);
}
};
void solve()
{
int q;
cin>>q;
Skip_List<int>lst(q+1);
int op,x;
while(q--)
{
cin>>op;
if(op==1)
{
cin>>x;
lst.insert(x);
}
else if(op==2)
{
cin>>x;
lst.erase(x);
}
else if(op==3)
{
cin>>x;
cout<<lst.rank(x)<<endl;
}
else if(op==4)
{
cin>>x;
cout<<lst.index(x)<<endl;
}
else if(op==5)
{
cin>>x;
cout<<lst.pre(x)<<endl;
}
else if(op==6)
{
cin>>x;
cout<<lst.post(x)<<endl;
}
}
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
唯一的问题就是在插入和删除时同步更新指针跨过的节点个数。考虑特判插入和删除时是否会导致新节点的产生,若不会的话,那么就只需要更新词频。这个在每次从 i 号节点第 h 层往右跳,最终的节点必然就是小于 num 的最右节点。若这个节点右侧就连着目标节点,那么就直接增加这个指针的词频。否则,就说明目标节点在当前层没有指针,那么就说明目标节点必然在当前节点往右的指针中,所以还是增加词频即可。
对于删除一个节点的问题,若往右跳完发现目标节点的层数达不到,那么还是直接减少指针的词频即可。否则的话先断连,然后让当前节点指针的词频加上目标节点指针的词频即可。
之后最麻烦的就是在插入一个新节点的部分时,因为在新插入一个节点的时候,这个节点会把原来的指针分为两部分,此时就需要更新这两部分跨过的节点个数。可以发现,在往右跳完以后,若目标节点的层数达不到,那么还是可以直接增加词频。否则,此时就需要求出当前节点到新插入的节点之间的个数,新插入节点指针跨过的个数可以通过原本的个数和前者相减求出。
既然需要求出下层跨过的节点个数,考虑让递归函数返回这个下层的个数 downCnt。有了这个值,就可以每次快速更新了。所以在第一层需要返回的就是往右跳过的个数 rightCnt,之后每次返回当前层往右跳的个数和下层的个数即可。
总结
这玩意儿也太妙了!