跳表学习笔记

跳表学习笔记


定义

跳表(跳跃表,Skip List),是一种基于有序链表的数据结构,通过引入随机化的思想,实现空间复杂度通常为 \(O(n)\),单次操作期望时间复杂度为 \(O(\log_2{n})\) 的插入、删除、查找、访问等功能。


原理

跳表的最基础实现如下图:(默认 升序 跳表,下同。)

无标题

最底层是原链表,但是我们发现有的节点拥有多层,这样使得这个链表在上层能够快速遍历。如果我们能够做到像倍增一样在第 \(i\) 层跳 \(2^i\) 步,那么快速查找与访问就很简单了,但是如果直接这样维护,我们无法让插入和删除也变得快速。

这个时候,为了解决这个问题,我们就可以引入分层和随机化的思想。

在将某个节点加入跳表的时候,从最低层开始,它有 \(p\in (0,1)\) 的概率(一般取 \(\frac{1}{2}\),有时也会取 \(\frac{1}{4}\))往上延伸一层,否则退出延伸。这样我们就可以解决上面需要精密设置每个节点层数的问题。

为了避免小概率溢出错误或复杂度过高,我们可以加一些限制,比如总层数严格限制在 \(O(\log_{\frac{1}{p}}{n})\)

复杂度分析

最高层数

跳表的底层就是原链表,设为\(0\),其点数为 \(n\),每往上一层,点数就缩小为上一层的 \(p\) 左右,第 \(i\) 层的期望节点数为 \(p^i n\)。我们设跳表的最上层有 \(x\) 个节点(\(x\) 一般为 \(2\)),那么最高层数 \(h\) 即可求出:

\[\begin{aligned} p^h n & = x \\ (\frac{1}{p})^h & = \frac{n}{x} \\ h & = \log_{\frac{1}{p}} {(\frac{n}{x})} \\ \end{aligned} \]

空间复杂度

单个结点最高层位于第 \(i\) 层的概率为 \(p^i(1-p)\),那么所有层的期望总结点数就为:

\[\begin{aligned} n [1 + \sum_{i \in \mathbb{N}} p^i(1-p) ] & = n[1 + (1-p) \frac{1}{1-p}] \\ & = 2n \\ \end{aligned} \]

(这里求一下等比数列求和公式极限即可。)

得到总结点数是 \(O(n)\) 级别的。那么如果我们用静态数组存储,空间复杂度为 \(O(n\log_{\frac{1}{p}}{n})\),用动态数组则是 \(O(n)\)

当然,最坏情况下都是 \(O(n\log_{\frac{1}{p}}{n})\)

时间复杂度

跳表有四个基本操作:插入、删除、查找、访问。它们的时间复杂度都基于 查找,所以我们主要分析它。

我们按照原论文来分析:按查找过程从后向前模拟,并分成两步。

  1. 将层数抬升到查找最高层 \(L(n)\)

    \(C(k)\) 为在无限列表向上爬升 \(k\) 级的预期成本。

    假设我们在某个结点的某一层,那么就会有两种情况:

    • 在结点的顶层:那么我们直接向该层的上一个结点移动。

      概率是:\(p\),还需成本 \(C(k-1)\)

    • 不在结点的顶层:再向上移动一层。

      概率是:\(1-p\),还需成本 \(C(k)\)

    \[\begin{cases} C(0) = 0 \\ C(k) = p [1 + C(k-1)] + (1-p) [1 + C(k)] \\ \end{cases} \]

    代入化简:

    \[\begin{aligned} C(k) & = p [1 + C(k-1)] + (1-p) [1 + C(k)] \\ C(k) & = C(k-1) + \frac{1}{p} \\ C(k) & = \frac{k}{p} \\ \end{aligned} \]

    所以当跳表最高层数为 \(L(n)\) 时,这一步期望步数就为 \(C(L(n)) = \frac{L(n)}{p}\)

  2. 到最高层后,分析还要走多少步:

    在最高层 \(L(n)\),期望点数为 \(n p^{L(n)}\),那么最多就走这么多步。

总期望时间复杂度为:

\[L(n) = \log_{\frac{1}{p}}{n} \\ \begin{aligned} \frac{L(n)}{p} + np^{L(n)} & = \frac{\log_{\frac{1}{p}}{n}}{p} + np^{\log_{\frac{1}{p}}{n}} \\ & = \frac{\log_{\frac{1}{p}}{n}}{p} + 1 \\ \end{aligned} \]

\(O(\log_{\frac{1}{p}}{n})\) 级别(与其它 \(\frac{L(n)-1}{p} + n p^{L(n)-1} = \frac{\log_{\frac{1}{p}}{n} - 1}{p} + \frac{1}{p}\) 无本质区别)。

\(p\) 的取值

\(p\) 的取值会影响算法的时空复杂度,我们可以通过决定它的取值来平衡。

原论文提供了一个表格:

\(p\) 标准化搜索时间(即标准化 \(\frac{L(n)}{p}\) 每个节点的平均指针个数(即 \(\frac{1}{1-p}\)
\(\frac{1}{2}\) \(1\) \(2\)
\(\frac{1}{e}\) \(0.94...\) \(1.58...\)
\(\frac{1}{4}\) \(1\) \(1.33...\)
\(\frac{1}{8}\) \(1.33...\) \(1.14...\)
\(\frac{1}{16}\) \(2\) \(1.07...\)

考虑到时空效率及随机效率,一般有两种选择:

  • \(p = \frac{1}{2}\),这时时间效率为主要因素;
  • \(p = \frac{1}{4}\),这时空间效率为主要因素。

实现

这里以一个单向跳表作为示例,外部采用静态数组存储结点,内部采用动态数组存储每层的下一个结点等信息。

建立跳表

基本限制

这里采用变量模版。

T 代表所存储的变量类型;N 表示最多结点数上限;P 用于调节上文中的 \(p\)lV 表示最高层数上限;rng 是所采用的随机数。

template<class T,const int N,const unsigned P,const int lV,mt19937 &rng>struct Skip_List {
    //......
};

设置结点

开一个结构体 node,存储每个结点的信息。

nxt 用来存储每层的下一个结点以及到下一个结点的距离(在跳表中的下标差值)。

同时,将两个结构体的 [] 都进行重载,加上 & 取地址符,可以使得代码更方便编写。

struct node {
	T val;
	vector<pair<int,int> > nxt;
	node() {}
	node(T val,int siz=0):val(val),nxt(vector<pair<int,int> >(siz,pair<int,int>(0,0))) {}
	pair<int,int> &operator[](int i) {
		return nxt[i];
	}
} a[N];
node &operator[](int i) {
	return a[i];
}

新建结点

这里涉及到 P 的取值对 \(p\) 的影响:

  1. P\(1\) 时,\(p =\frac{1}{2}\)
  2. P\(3\) 时,\(p =\frac{1}{4}\)

st 是用于空间回收的栈数组;tot 是当前的结点总数;siz 是单个结点的最高层数。

int New(T val) {
	int idx(top?st[top--]:++tot),siz(1);
	while(siz<lV&&!(rng()&P))++siz;
	return a[idx]=node(val,siz),idx;
}

初始化

我们把结点 \(0\) 作为链头与链尾,注意这里到下一个的距离应设为 \(1\)

void Init() {
	a[top=tot=0].nxt=vector<pair<int,int> >(lV,pair<int,int>(0,1));
}

按 val(值)查找

查找较简单,我们直接通过类似倍增的方法即可:从高层到低层,只要下一个结点满足条件,就往后跳。但是依旧有一些细节需要注意。

Lower_bound(val)

这里先实现一个 Lower_bound(val),返回结点编号:

int Lower_bound(T val) {
	int cur(0);
	DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<val)cur=a[cur][i].first;
	return a[cur][0].first;
}

这里的流程就是:从高层到低层,只要下一个结点小于 \(val\),就往后跳,最后返回得到的编号的下一个结点。

如图,当我们的 \(val=15\) 时,查找过程就长这样:

无标题 (2)

Upper_bound(val)

同理,只需把 < 换成 <= 即可:

int Upper_bound(T val) {
	int cur(0);
	DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<=val)cur=a[cur][i].first;
	return a[cur][0].first;
}

Prev(val) & Next(val)

查询前驱后继的实现只需稍加改动:

int Prev(T val) {
	int cur(0);
	DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<val)cur=a[cur][i].first;
	return cur;
}
int Next(T val) {
	int cur(0);
	DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<=val)cur=a[cur][i].first;
	return a[cur][0].first;
}

按 val 插入

一般的插入只需找到结点并更新它们的前后结点关系即可,但是我们为了实现随机访问,加入了前后距离差距,所以这里也需要更新,还有删除操作也是同理。

int Insert(T val) {
	vector<pair<int,int> > last(lV,pair<int,int>(0,0));
	int cur(0),sum(0);
	/*第一步仍是查找,只不过其中再加入了一些记录*/
	DOR(i,lV-1,0) {
		while(a[cur][i].first&&a[a[cur][i].first].val<val)
			last[i].second+=a[cur][i].second,cur=a[cur][i].first;
		last[i].first=cur;
	}
	/*调用 New() 新建结点*/
	cur=New(val);
	FOR(i,0,a[cur].nxt.size()-1) {
		/*sum 在这里表示:该层最后遍历的结点到新建结点的距离 - 1*/
		/*更新当前结点的下一个结点与到它的距离*/
		a[cur][i]=pair<int,int>(a[last[i].first][i].first,a[last[i].first][i].second-sum);
		/*更新每层上一个结点的下一个结点与到它的距离*/
		a[last[i].first][i]=pair<int,int>(cur,sum+1),sum+=last[i].second;
	}
	/*别忘了上面高层的距离还要加 1*/
	FOR(i,a[cur].nxt.size(),lV-1)++a[last[i].first][i].second;
	return cur;
}

开了一个动态数组 last,用于存储每层中在它之前的结点编号与到上一层最后遍历的结点的距离,这样我们可以倒着从低层到高层用前缀和来进行更新。

举个例子:在下面的跳表中插入一个 \(7\),层数为 \(2\)

无标题

他会变成下面这个样子:

无标题 (1)

按 val 删除

与按 val 插入基本相同,但是要注意空间回收。

int Erase(T val) {
	vector<pair<int,int> > last(lV,pair<int,int>(0,0));
	int cur(0),sum(0);
	DOR(i,lV-1,0) {
		while(a[cur][i].first&&a[a[cur][i].first].val<val)
			last[i].second+=a[cur][i].second,cur=a[cur][i].first;
		last[i].first=cur;
	}
	if(a[cur=a[cur][0].first].val!=val)return cur;
	FOR(i,0,a[cur].nxt.size()-1)
		a[last[i].first][i]=pair<int,int>(a[cur][i].first,sum+a[cur][i].second),sum+=last[i].second;
	FOR(i,a[cur].nxt.size(),lV-1)--a[last[i].first][i].second;
	return a[st[++top]=cur][0].first;
}

随机访问

即按 key(键)访问,在我们处理好所有节点在每层到下一个结点的距离后,这就非常容易实现了。

int At(int key) {
	int cur(0);
	DOR(i,lV-1,0)while(a[cur][i].first&&a[cur][i].second<=key)key-=a[cur][i].second,cur=a[cur][i].first;
	return cur;
}

与线段树上二分类似,从高层到底层,只要 key 的值还大于到下一个结点的距离,就不停的往后遍历,最后返回结点的编号。

按 val(值)查询排名

借助用于随机访问处理好的信息,我们也可以实现这个操作:

int Rank(int val) {
	int cur(0),sum(0);
	DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<val)
		sum+=a[cur][i].second,cur=a[cur][i].first;
	return sum+1;
}

合并与分裂

其实这两个都是可以 \(O(\log{n})\) 做到的,只是有点麻烦。

打标记

这个也可以做,也是 \(O(\log{n})\) 的。

完整代码

template<class T,const int N,const unsigned P,const int lV,mt19937 &rng>struct Skip_List {
	int top,tot;
	int st[N];
	struct node {
		T val;
		vector<pair<int,int> > nxt;
		node() {}
		node(T val,int siz=0):val(val),nxt(vector<pair<int,int> >(siz,pair<int,int>(0,0))) {}
		pair<int,int> &operator[](int i) {
			return nxt[i];
		}
	} a[N];
	node &operator[](int i) {
		return a[i];
	}
	Skip_List() {
		Init();
	}
	void Init() {
		a[top=tot=0].nxt=vector<pair<int,int> >(lV,pair<int,int>(0,1));
	}
	int New(T val) {
		int idx(top?st[top--]:++tot),siz(1);
		while(siz<lV&&!(rng()&P))++siz;
		return a[idx]=node(val,siz),idx;
	}
	int Prev(T val) {
		int cur(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<val)cur=a[cur][i].first;
		return cur;
	}
	int Next(T val) {
		int cur(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<=val)cur=a[cur][i].first;
		return a[cur][0].first;
	}
	int Lower_bound(T val) {
		int cur(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<val)cur=a[cur][i].first;
		return a[cur][0].first;
	}
	int Upper_bound(T val) {
		int cur(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<=val)cur=a[cur][i].first;
		return a[cur][0].first;
	}
	int Insert(T val) {
		vector<pair<int,int> > last(lV,pair<int,int>(0,0));
		int cur(0),sum(0);
		DOR(i,lV-1,0) {
			while(a[cur][i].first&&a[a[cur][i].first].val<val)
				last[i].second+=a[cur][i].second,cur=a[cur][i].first;
			last[i].first=cur;
		}
		cur=New(val);
		FOR(i,0,a[cur].nxt.size()-1) {
			a[cur][i]=pair<int,int>(a[last[i].first][i].first,a[last[i].first][i].second-sum);
			a[last[i].first][i]=pair<int,int>(cur,sum+1),sum+=last[i].second;
		}
		FOR(i,a[cur].nxt.size(),lV-1)++a[last[i].first][i].second;
		return cur;
	}
	int Erase(T val) {
		vector<pair<int,int> > last(lV,pair<int,int>(0,0));
		int cur(0),sum(0);
		DOR(i,lV-1,0) {
			while(a[cur][i].first&&a[a[cur][i].first].val<val)
				last[i].second+=a[cur][i].second,cur=a[cur][i].first;
			last[i].first=cur;
		}
		if(a[cur=a[cur][0].first].val!=val)return cur;
		FOR(i,0,a[cur].nxt.size()-1)
			a[last[i].first][i]=pair<int,int>(a[cur][i].first,sum+a[cur][i].second),sum+=last[i].second;
		FOR(i,a[cur].nxt.size(),lV-1)--a[last[i].first][i].second;
		return a[st[++top]=cur][0].first;
	}
	int At(int key) {
		int cur(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[cur][i].second<=key)key-=a[cur][i].second,cur=a[cur][i].first;
		return cur;
	}
	int Rank(int val) {
		int cur(0),sum(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<val)
			sum+=a[cur][i].second,cur=a[cur][i].first;
		return sum+1;
	}
};

应用

可以代替普通平衡树,std::mapstd::setstd::multiset 等数据结构,同时在常数上占优势,而且方便调试。

P3369 【模板】普通平衡树

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define ll long long
#define RCL(a,b,c,d) memset(a,b,sizeof(c)*(d))
#define FOR(i,a,b) for(int i(a);i<=(int)(b);++i)
#define DOR(i,a,b) for(int i(a);i>=(int)(b);--i)
#define tomax(a,...) ((a)=max({(a),__VA_ARGS__}))
#define tomin(a,...) ((a)=min({(a),__VA_ARGS__}))
#define EDGE(g,i,x,y) for(int i=(g).h[(x)],y=(g)[(i)].v;~i;y=(g)[i=(g)[i].nxt].v)
#define main Main();signed main(){ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);return Main();}signed Main
using namespace std;
constexpr int N(1e5+10),lN(17);
int n;
random_device rd;
mt19937 rng(rd());
template<class T,const int N,const unsigned P,const int lV,mt19937 &rng>struct Skip_List {
	int siz,top,tot;
	int st[N];
	struct node {
		T val;
		vector<pair<int,int> > nxt;
		node() {}
		node(T val,int siz=0):val(val),nxt(vector<pair<int,int> >(siz,pair<int,int>(0,0))) {}
		pair<int,int> &operator[](int i) {
			return nxt[i];
		}
	} a[N];
	node &operator[](int i) {
		return a[i];
	}
	Skip_List() {
		Init();
	}
	void Init() {
		a[top=tot=0].nxt=vector<pair<int,int> >(lV,pair<int,int>(0,1));
	}
	int New(T val) {
		int idx(top?st[top--]:++tot),siz(1);
		while(siz<lV&&!(rng()&P))++siz;
		return a[idx]=node(val,siz),idx;
	}
	int Prev(T val) {
		int cur(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<val)cur=a[cur][i].first;
		return cur;
	}
	int Next(T val) {
		int cur(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<=val)cur=a[cur][i].first;
		return a[cur][0].first;
	}
	int Lower_bound(T val) {
		int cur(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<val)cur=a[cur][i].first;
		return a[cur][0].first;
	}
	int Upper_bound(T val) {
		int cur(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<=val)cur=a[cur][i].first;
		return a[cur][0].first;
	}
	int Insert(T val) {
		vector<pair<int,int> > last(lV,pair<int,int>(0,0));
		int cur(0),sum(0);
		DOR(i,lV-1,0) {
			while(a[cur][i].first&&a[a[cur][i].first].val<val)
				last[i].second+=a[cur][i].second,cur=a[cur][i].first;
			last[i].first=cur;
		}
		cur=New(val);
		FOR(i,0,a[cur].nxt.size()-1) {
			a[cur][i]=pair<int,int>(a[last[i].first][i].first,a[last[i].first][i].second-sum);
			a[last[i].first][i]=pair<int,int>(cur,sum+1),sum+=last[i].second;
		}
		FOR(i,a[cur].nxt.size(),lV-1)++a[last[i].first][i].second;
		return cur;
	}
	int Erase(T val) {
		vector<pair<int,int> > last(lV,pair<int,int>(0,0));
		int cur(0),sum(0);
		DOR(i,lV-1,0) {
			while(a[cur][i].first&&a[a[cur][i].first].val<val)
				last[i].second+=a[cur][i].second,cur=a[cur][i].first;
			last[i].first=cur;
		}
		if(a[cur=a[cur][0].first].val!=val)return cur;
		FOR(i,0,a[cur].nxt.size()-1)
			a[last[i].first][i]=pair<int,int>(a[cur][i].first,sum+a[cur][i].second),sum+=last[i].second;
		FOR(i,a[cur].nxt.size(),lV-1)--a[last[i].first][i].second;
		return a[st[++top]=cur][0].first;
	}
	int At(int key) {
		int cur(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[cur][i].second<=key)key-=a[cur][i].second,cur=a[cur][i].first;
		return cur;
	}
	int Rank(int val) {
		int cur(0),sum(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<val)
			sum+=a[cur][i].second,cur=a[cur][i].first;
		return sum+1;
	}
};
Skip_List<int,N,1,lN,rng> lis;
signed main() {
	cin>>n;
	while(n--) {
		int opt,x;
		cin>>opt>>x;
		switch(opt) {
			case 1: {
				lis.Insert(x);
				break;
			}
			case 2: {
				lis.Erase(x);
				break;
			}
			case 3: {
				cout<<lis.Rank(x)<<endl;
				break;
			}
			case 4: {
				cout<<lis[lis.At(x)].val<<endl;
				break;
			}
			case 5: {
				cout<<lis[lis.Prev(x)].val<<endl;
				break;
			}
			case 6: {
				cout<<lis[lis.Next(x)].val<<endl;
				break;
			}
		}
	}
	return 0;
}

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

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define ll long long
#define RCL(a,b,c,d) memset(a,b,sizeof(c)*(d))
#define FOR(i,a,b) for(int i(a);i<=(int)(b);++i)
#define DOR(i,a,b) for(int i(a);i>=(int)(b);--i)
#define tomax(a,...) ((a)=max({(a),__VA_ARGS__}))
#define tomin(a,...) ((a)=min({(a),__VA_ARGS__}))
#define EDGE(g,i,x,y) for(int i=(g).h[(x)],y=(g)[(i)].v;~i;y=(g)[i=(g)[i].nxt].v)
#define main Main();signed main(){ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);return Main();}signed Main
using namespace std;
constexpr int N(1e6+1e5+10),lN(20);
int n,m,ans,Ans;
random_device rd;
mt19937 rng(rd());
template<class T,const int N,const unsigned P,const int lV,mt19937 &rng>struct Skip_List {
	int siz,top,tot;
	int st[N];
	struct node {
		T val;
		vector<pair<int,int> > nxt;
		node() {}
		node(T val,int siz=0):val(val),nxt(vector<pair<int,int> >(siz,pair<int,int>(0,0))) {}
		pair<int,int> &operator[](int i) {
			return nxt[i];
		}
	} a[N];
	node &operator[](int i) {
		return a[i];
	}
	Skip_List() {
		Init();
	}
	void Init() {
		a[top=tot=0].nxt=vector<pair<int,int> >(lV,pair<int,int>(0,1));
	}
	int New(T val) {
		int idx(top?st[top--]:++tot),siz(1);
		while(siz<lV&&!(rng()&P))++siz;
		return a[idx]=node(val,siz),idx;
	}
	int Prev(T val) {
		int cur(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<val)cur=a[cur][i].first;
		return cur;
	}
	int Next(T val) {
		int cur(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<=val)cur=a[cur][i].first;
		return a[cur][0].first;
	}
	int Lower_bound(T val) {
		int cur(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<val)cur=a[cur][i].first;
		return a[cur][0].first;
	}
	int Upper_bound(T val) {
		int cur(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<=val)cur=a[cur][i].first;
		return a[cur][0].first;
	}
	int Insert(T val) {
		vector<pair<int,int> > last(lV,pair<int,int>(0,0));
		int cur(0),sum(0);
		DOR(i,lV-1,0) {
			while(a[cur][i].first&&a[a[cur][i].first].val<val)
				last[i].second+=a[cur][i].second,cur=a[cur][i].first;
			last[i].first=cur;
		}
		cur=New(val);
		FOR(i,0,a[cur].nxt.size()-1) {
			a[cur][i]=pair<int,int>(a[last[i].first][i].first,a[last[i].first][i].second-sum);
			a[last[i].first][i]=pair<int,int>(cur,sum+1),sum+=last[i].second;
		}
		FOR(i,a[cur].nxt.size(),lV-1)++a[last[i].first][i].second;
		return cur;
	}
	int Erase(T val) {
		vector<pair<int,int> > last(lV,pair<int,int>(0,0));
		int cur(0),sum(0);
		DOR(i,lV-1,0) {
			while(a[cur][i].first&&a[a[cur][i].first].val<val)
				last[i].second+=a[cur][i].second,cur=a[cur][i].first;
			last[i].first=cur;
		}
		if(a[cur=a[cur][0].first].val!=val)return cur;
		FOR(i,0,a[cur].nxt.size()-1)
			a[last[i].first][i]=pair<int,int>(a[cur][i].first,sum+a[cur][i].second),sum+=last[i].second;
		FOR(i,a[cur].nxt.size(),lV-1)--a[last[i].first][i].second;
		return a[st[++top]=cur][0].first;
	}
	int At(int key) {
		int cur(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[cur][i].second<=key)key-=a[cur][i].second,cur=a[cur][i].first;
		return cur;
	}
	int Rank(int val) {
		int cur(0),sum(0);
		DOR(i,lV-1,0)while(a[cur][i].first&&a[a[cur][i].first].val<val)
			sum+=a[cur][i].second,cur=a[cur][i].first;
		return sum+1;
	}
};
Skip_List<int,N,3,lN,rng> lis;
signed main() {
	cin>>n>>m;
	FOR(i,1,n){
		int a;
		cin>>a,lis.Insert(a);
	}
	while(m--) {
		int opt,x;
		cin>>opt>>x,x^=ans;
		switch(opt) {
			case 1: {
				lis.Insert(x);
				break;
			}
			case 2: {
				lis.Erase(x);
				break;
			}
			case 3: {
				Ans^=(ans=lis.Rank(x));
				break;
			}
			case 4: {
				Ans^=(ans=lis[lis.At(x)].val);
				break;
			}
			case 5: {
				Ans^=(ans=lis[lis.Prev(x)].val);
				break;
			}
			case 6: {
				Ans^=(ans=lis[lis.Next(x)].val);
				break;
			}
		}
	}
	cout<<Ans<<endl;
	return 0;
}

P3991 [BJOI2017] 喷式水战改

可以用跳表实现一个简单的动态 DP,思路类似无旋的 Treap。

大概的思路见 2009 国家集训队论文——李骥扬《线段跳表——跳表的一个拓展》。

//#define Plus_Cat ""
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define ll long long
#define RCL(a,b,c,d) memset(a,b,sizeof(c)*(d))
#define tomin(a,...) ((a)=min({(a),__VA_ARGS__}))
#define tomax(a,...) ((a)=max({(a),__VA_ARGS__}))
#define FOR(i,a,b) for(int i(a); i<=(int)(b); ++i)
#define DOR(i,a,b) for(int i(a); i>=(int)(b); --i)
#define EDGE(g,i,x,y) for(int i(g.h[x]),y(g[i].v); ~i; y=g[i=g[i].nxt].v)
using namespace std;
constexpr int N(1e5+10),lN(18),lV(lN+1);

#define DE(...) E(#__VA_ARGS__,__VA_ARGS__)
struct Ecat {

	template<class T>void operator ()(const char *fmt,const T x) {
		cerr<<fmt<<':'<<x<<'.'<<endl;
	}

	template<class T,class...Types>void operator ()(const char *fmt,const T x,const Types...args) {
		while(*fmt^',')cerr<<*fmt++;
		return cerr<<':'<<x<<" ,",(*this)(++fmt,args...);
	}

} E;

int n;
int a[N],b[N],c[N];
ll ans;
mt19937 rng(random_device {}());
//mt19937 rng(20080929);
struct Mat {
	ll d[4][4];

	ll *operator [](int i) { return d[i]; }

	Mat() {
		FOR(i,0,3)FOR(j,i,3)d[i][j]=0;
	}

	Mat(int id,int x) {
		d[0][0]=(ll)x*a[id],d[1][1]=(ll)x*b[id],d[2][2]=(ll)x*c[id],d[3][3]=(ll)x*a[id];
		FOR(l,0,3)FOR(r,l+1,3)d[l][r]=max({d[l][r-1],d[r][r]});
	}

	friend Mat operator *(Mat A,Mat B) {
		Mat C;
		FOR(i,0,3)FOR(j,i,3)FOR(k,i,j)tomax(C[i][j],A[i][k]+B[k][j]);
		return C;
	}

};
struct Skip_List {
	int tot;
	int Id[N<<1];
	struct node {
		int nxt;
		ll len;
		Mat mat;

		node(int nxt=0,ll len=0,Mat mat=Mat()):nxt(nxt),len(len),mat(mat) {}

		void Update(node B) {
			len+=B.len,mat=mat*B.mat;
		}

	};
	vector<node> tr[N<<1];

	Skip_List() { Init(); }

	void Init() {
		tr[tot=0]=vector<node>(lV,node());
	}

	int New(int id,int x) {
		int siz(2);
		while(siz<lV&&!(rng()&1))++siz;
		return Id[++tot]=id,tr[tot]=vector<node>(siz,node(0,x,Mat(id,x))),tot;
	}

	void Insert(ll Len,int id,int x) {
		array<int,lV> tmp;
		stack<array<int,2>,vector<array<int,2> > > st;
		int p(0);
		ll len(0);
		DOR(i,lN,1) {
			while(tr[p][i].nxt&&len+tr[p][i-1].len<Len)
				len+=tr[p][i-1].len,st.push({p,i}),p=tr[p][i].nxt;
			st.push({p,i}),tmp[i]=p;
		}
		auto insert=[&](int id,int x) {
			int cur(New(id,x));
			tr[cur][0].nxt=tr[p][0].nxt,tr[p][0].nxt=cur;
			FOR(i,1,tr[cur].size()-1) {
				tr[cur][i].nxt=tr[tmp[i]][i].nxt,tr[tmp[i]][i].nxt=cur;
				tr[cur][i].len=tr[cur][i-1].len,tr[cur][i].mat=tr[cur][i-1].mat;
				if(tr[cur][i].nxt&&i==(int)tr[tr[cur][i].nxt].size()-1)
					tr[cur][i].Update(tr[tr[cur][i].nxt][i]);
			}
			FOR(i,tr[cur].size(),lN)tr[tmp[i]][i].len+=x;
		};
		if(len+tr[p][0].len<=Len)insert(id,x);
		else {
			int del(len+tr[p][0].len-Len);
			tr[p][0].mat=Mat(Id[p],tr[p][0].len-=del);
			FOR(i,1,lN)tr[tmp[i]][i].len-=del;
			insert(Id[p],del),insert(id,x);
		}
		while(!st.empty()) {
			int p(st.top()[0]),dep(st.top()[1]);
			st.pop(),tr[p][dep].len=tr[p][dep-1].len,tr[p][dep].mat=tr[p][dep-1].mat;
			if(tr[p][dep].nxt&&dep==(int)tr[tr[p][dep].nxt].size()-1)
				tr[p][dep].Update(tr[tr[p][dep].nxt][dep]);
		}
	}

} sl;

int main() {
#ifdef Plus_Cat
	freopen(Plus_Cat ".in","r",stdin),freopen(Plus_Cat ".out","w",stdout);
#endif
	cin>>n;
	FOR(i,1,n) {
		ll p;
		int x;
		cin>>p>>a[i]>>b[i]>>c[i]>>x,sl.Insert(p,i,x);
		ll res(sl.tr[0][lN].mat[0][3]);
		cout<<res-ans<<endl,ans=res;
	}
	return 0;
}

参考资料

  1. 手把手带你实现跳表 - 伞的隙间 (tibrella.space)
  2. 跳表 - OI Wiki (oi-wiki.org)
  3. skiplists (cmu.edu)

posted @ 2024-10-10 14:56  Add_Catalyst  阅读(110)  评论(0)    收藏  举报