基本概念
\(Splay\) 是一种 平衡树 ,由 \(Daniel \ Sleator\) 和 \(Robert \ Tarjan\) 提出。它可以维护普通的二叉搜索树所支持的操作,也可以作为 \(LCT\) 的辅助树,进行很多复杂的操作。\(Splay\) 是两种最常用的平衡树之一,因为其在作为 \(LCT\) 的辅助树时比 \(fhq \ treap\) 快了一个 \(log\) ,所以有不少大佬钟情于它。
\(Splay\) 的主要思想同样是利用 旋转 ,与 \(Treap\) 不同的地方在于 \(Splay\) 不会给每个结点另外附上一个随机权值,而是在每一次操作过后将被操作的结点旋转到根结点,在此过程中顺便维护树的平衡。如果数据足够弱,多次连续的操作针对同一个结点,\(Splay\) 的性能甚至可以超越 \(Treap\) 和 \(fhq \ Treap\) 。
\(Splay\) 算法思想优美,代码较为直观,但是在初学时不易掌握。因此,本文将尽量详细讲解,让您较为轻松地学会这种 毒瘤的 数据结构。
基本操作
清空( \(clear\) )
清空某个结点的所有信息。
void clear(int x) {
son[x][0] = son[x][1] = size[x] = val[x] = cnt[x] = fa[x] = 0;
}
更新( \(update\) )
更新结点 \(x\) 的子树大小。
值得注意的是,因为 \(Splay\) 经常会对不存在的结点 \(0\) 进行操作,所以在访问某个结点的时候经常需要判断其是否真实存在。
void update(int x) {
if (x) {
size[x] = cnt[x];
if (son[x][0]) {
size[x] += size[son[x][0]];
}
if (son[x][1]) {
size[x] += size[son[x][1]];
}
}
}
判断( \(get\) )
判断结点 \(x\) 是其父亲的左儿子( \(0\) )还是右儿子( \(1\) )。
bool get(int x) {
return son[fa[x]][1] == x;
}
双旋( \(rotate\) )
如果还没有学习过 \(Treap\) 或 \(AVL\) 中的左旋和右旋操作,您可以点击 这篇博文 来进行初步的认识。
回顾 \(Treap\) 中的旋转操作,我们每次只针对一个结点进行旋转操作。但是在 \(Splay\) 中,我们每次操作需要旋转多个结点,还要维护每个结点的父亲结点。因此,双旋操作应运而生。分类讨论三种情况:
假如我们要将 \(x\) 旋转到 \(g\) 的位置。令当前结点为 \(x\) ,其父结点为 \(f\) ,祖先结点为 \(g\) 。如果 \(x\) 是 \(f\) 的左儿子,且 \(f\) 也是 \(g\) 的左儿子;或者 \(x\) 是 \(f\) 的右儿子,且 \(f\) 也是 \(g\) 的右儿子,即父子方向统一,则此时必须先旋转 \(f\) ,再旋转 \(x\) ,否则 \(Splay\) 会失去平衡。具体读者可以自行在草稿纸上模拟,本文限于篇幅不再赘述。
如果 \(x\) 是 \(f\) 的右儿子,且 \(f\) 是 \(g\) 的左儿子;或者 \(x\) 是 \(f\) 的左儿子,且 \(f\) 是 \(g\) 的右儿子,即父子方向不统一,此时先将 \(x\) 旋转到 \(f\) 的位置,再从 \(f\) 的位置旋转到 \(g\) 的位置即可。模拟表明这样旋转不会失衡。
最后,如果 \(f\) 就是根结点,此时不需要双旋,直接单旋 \(x\) 到根结点的位置即可。有了双旋操作,我们就可以完成下面的 \(Splay\) 操作。
void rotate(int x) {
int y = fa[x], z = fa[y], k = get(x);
son[y][k] = son[x][k ^ 1];
fa[son[y][k]] = y;
son[x][k ^ 1] = y;
fa[y] = x;
fa[x] = z;
if (z) {
son[z][son[z][1] == y] = x;
}
update(y);
update(x);
}
伸展( \(Splay\) )
\(Splay\) 操作即为将某个结点 \(x\) 一路向上旋转,直到成为另一个结点 \(goal\) 的子结点。
\(Splay\) 操作的难点在于,将结点一路向上旋转可能会破坏平衡树原本的平衡性质,从而被数据卡成 \(TLE\) 。联系上述的双旋操作,因为无论是哪种情况,双旋操作都可以保持树的平衡,所以我们可以将 \(Splay\) 操作拆分成多次双旋操作。
具体地,假设当前结点为 \(x\) ,若 \(x\) 的父亲结点和祖先结点都不是 \(goal\) ,此时直接进行双旋操作即可。若 \(x\) 的祖先结点为 \(goal\) ,说明此时直接进行一次单旋即可。若 \(x\) 的父亲结点为 \(goal\) ,说明 \(x\) 已经是 \(goal\) 的子结点了,直接退出。另外,如果 \(goal = 0\) ,我们将其看作为把 \(x\) 旋转到根结点,直接特判。
根据以上基本逻辑,可以写出如下代码:
void splay(int x, int goal) {
for (int f; (f = fa[x]) != goal; rotate(x)) {
if (fa[f] != goal) {
rotate(get(x) == get(f) ? f : x);
}
}
if (!goal) {
root = x;
}
}
还可以继续偷一下懒。因为在模板题中只会将 \(x\) 旋转到根结点,所以我们可以默认 \(goal = 0\) ,写出如下代码:
void splay(int x) {
for (int f; (f = fa[x]); rotate(x)) {
if (fa[f]) {
rotate(get(x) == get(f) ? f : x);
}
}
root = x;
}
插入( \(insert\) )
在树中插入一个值为 \(x\) 的新结点。
假如全树的根结点 \(root = 0\) ,说明这棵树是空树,新建一个结点并将其设为根结点即可。
否则,从根结点开始查找。若当前结点的权值等于 \(x\) ,说明之前已经插入过值相同的结点,令其个数加一,并更新该结点及其父结点,并将该结点伸展到根结点即可。
\(Q:\) 为什么要更新其父结点?
\(A:\) 因为 \(Splay\) 操作的时候可能会出问题。
反之,在相应的子树内查找。如果此时发现该子树为空树,说明该值在树中并不存在,直接新建一个结点,更新其父结点并将其旋转到根结点。
void insert(int x) {
if (root == 0) {
tot++;
val[tot] = x;
cnt[tot] = size[tot] = 1;
son[tot][0] = son[tot][1] = fa[tot] = 0;
root = tot;
return;
}
int u = root, f = 0;
while (true) {
if (x == val[u]) {
cnt[u]++;
update(u);
update(f);
splay(u);
break;
}
f = u;
u = son[u][x > val[u]];
if (u == 0) {
tot++;
fa[tot] = f;
son[tot][0] = son[tot][1] = 0;
son[f][x > val[f]] = tot;
cnt[tot] = size[tot] = 1;
val[tot] = x;
update(f);
splay(tot);
break;
}
}
}
排名( \(rank\) )
查询值 \(x\) 在树中的排名。
按照正常的二叉搜索树思路思考即可。如果 \(x <\) 当前结点的权值,查询 \(x\) 在左子树内的排名。反之,如果 \(x\) 存在左子树,则令排名加上左子树的大小。此时若 \(x =\) 当前结点的权值,直接返回当前排名 \(+ 1\) 并将当前结点旋转到根结点。如果 \(x >\) 当前结点的权值,则令排名继续加上当前结点的个数,并在右子树内继续查找。
int rank(int x) {
int u = root, ans = 0;
while (true) {
if (x < val[u]) {
u = son[u][0];
} else {
if (son[u][0]) {
ans += size[son[u][0]];
}
if (x == val[u]) {
splay(u);
return ans + 1;
}
ans += cnt[u];
u = son[u][1];
}
}
}
查找( \(find\) )
查找树中排名为 \(x\) 的值的编号。
假如当前结点的左子树不为空并且左子树的大小 \(\geq x\) ,在左子树内继续查找;否则,若 \(x \leq\) 左子树的大小加上当前结点的个数,直接返回当前结点的编号;反之,令排名减去左子树的大小 \(+\) 当前结点的个数,继续在右子树内查找。
int find(int x) {
int u = root;
while (true) {
if (son[u][0] && x <= size[son[u][0]]) {
u = son[u][0];
} else {
int sz = (son[u][0] ? size[son[u][0]] : 0) + cnt[u];
if (x <= sz) {
return val[u];
}
x -= sz;
u = son[u][1];
}
}
}
前驱、后继( \(pre, nxt\) )
查找树中 \(x\) 的前驱的编号。
不管树中是否存在 \(x\),先将 \(x\) 插入到树中,并将 \(x\) 旋转到根结点。此时 \(x\) 的左子树一定全部小于 \(x\) ,右子树一定全部大于 \(x\)。前驱就是左子树内右下角的结点,后继就是右子树内左下角的结点,模拟即可。注意最后还要删去 \(x\) 。
int pre() {
int u = son[root][0];
while (son[u][1]) {
u = son[u][1];
}
return u;
}
//调用入口
//insert(x);
//printf("%d\n", val[pre()]);
//del(x);
int nxt() {
int u = son[root][1];
while (son[u][0]) {
u = son[u][0];
}
return u;
}
//调用入口
//insert(x);
//printf("%d\n", val[nxt()]);
//del(x);
删除( \(del\) )
删除树中值为 \(x\) 的结点,若有多个,只删一个。
先利用 rank
函数将 \(x\) 旋转到根结点,再分类讨论:如果 \(x\) 被多次插入,删除其中一个并更新即可;如果 \(x\) 是叶子节点,直接清空 \(x\) 并将 \(root\) 赋值为 \(0\) 表示删除后是空树;如果 \(x\) 只有左儿子,将左儿子赋为新根并清空 \(x\),只有右儿子同理;反之,将 \(x\) 的前驱伸展到根结点,左子树其他的结点按兵不动,再将 \(x\) 的右子树连接在前驱的右子树上。此时构造出的新树符合二叉搜索树的性质,直接清空 \(x\) 并更新。
void del(int x) {
rank(x);
if (cnt[root] > 1) {
cnt[root]--;
update(root);
return;
}
if (!son[root][0] && !son[root][1]) {
clear(root);
root = 0;
return;
}
if (!son[root][0]) {
int rt = root;
root = son[root][1];
fa[root] = 0;
clear(rt);
return;
}
if (!son[root][1]) {
int rt = root;
root = son[root][0];
fa[root] = 0;
clear(rt);
return;
}
int p = pre(), rt = root;
splay(p);
son[root][1] = son[rt][1];
fa[son[rt][1]] = root;
clear(rt);
update(root);
}
参考代码
#include <cstdio>
using namespace std;
#define rank Rank
const int maxn = 1e5 + 5;
int n, root, tot;
int fa[maxn], son[maxn][2];
int cnt[maxn], val[maxn], size[maxn];
void clear(int x) {
son[x][0] = son[x][1] = size[x] = val[x] = cnt[x] = fa[x] = 0;
}
bool get(int x) {
return son[fa[x]][1] == x;
}
void update(int x) {
if (x) {
size[x] = cnt[x];
if (son[x][0]) {
size[x] += size[son[x][0]];
}
if (son[x][1]) {
size[x] += size[son[x][1]];
}
}
}
void rotate(int x) {
int y = fa[x], z = fa[y], k = get(x);
son[y][k] = son[x][k ^ 1];
fa[son[y][k]] = y;
son[x][k ^ 1] = y;
fa[y] = x;
fa[x] = z;
if (z) {
son[z][son[z][1] == y] = x;
}
update(y);
update(x);
}
void splay(int x) {
for (int f = 0; (f = fa[x]); rotate(x)) {
if (fa[f]) {
rotate((get(x) == get(f)) ? f : x);
}
}
root = x;
}
void insert(int x) {
if (root == 0) {
tot++;
val[tot] = x;
cnt[tot] = size[tot] = 1;
son[tot][0] = son[tot][1] = fa[tot] = 0;
root = tot;
return;
}
int u = root, f = 0;
while (true) {
if (x == val[u]) {
cnt[u]++;
update(u);
update(f);
splay(u);
break;
}
f = u;
u = son[u][x > val[u]];
if (u == 0) {
tot++;
fa[tot] = f;
son[tot][0] = son[tot][1] = 0;
son[f][x > val[f]] = tot;
cnt[tot] = size[tot] = 1;
val[tot] = x;
update(f);
splay(tot);
break;
}
}
}
int rank(int x) {
int u = root, ans = 0;
while (true) {
if (x < val[u]) {
u = son[u][0];
} else {
if (son[u][0]) {
ans += size[son[u][0]];
}
if (x == val[u]) {
splay(u);
return ans + 1;
}
ans += cnt[u];
u = son[u][1];
}
}
}
int find(int x) {
int u = root;
while (true) {
if (son[u][0] && x <= size[son[u][0]]) {
u = son[u][0];
} else {
int sz = (son[u][0] ? size[son[u][0]] : 0) + cnt[u];
if (x <= sz) {
return val[u];
}
x -= sz;
u = son[u][1];
}
}
}
int pre() {
int u = son[root][0];
while (son[u][1]) {
u = son[u][1];
}
return u;
}
int nxt() {
int u = son[root][1];
while (son[u][0]) {
u = son[u][0];
}
return u;
}
void del(int x) {
rank(x);
if (cnt[root] > 1) {
cnt[root]--;
update(root);
return;
}
if (!son[root][0] && !son[root][1]) {
clear(root);
root = 0;
return;
}
if (!son[root][0]) {
int rt = root;
root = son[root][1];
fa[root] = 0;
clear(rt);
return;
}
if (!son[root][1]) {
int rt = root;
root = son[root][0];
fa[root] = 0;
clear(rt);
return;
}
int p = pre(), rt = root;
splay(p);
son[root][1] = son[rt][1];
fa[son[rt][1]] = root;
clear(rt);
update(root);
}
int main() {
int opt, x;
scanf("%d", &n);
while (n--) {
scanf("%d%d", &opt, &x);
if (opt == 1) {
insert(x);
} else if (opt == 2) {
del(x);
} else if (opt == 3) {
printf("%d\n", rank(x));
} else if (opt == 4) {
printf("%d\n", find(x));
} else if (opt == 5) {
insert(x);
printf("%d\n", val[pre()]);
del(x);
} else {
insert(x);
printf("%d\n", val[nxt()]);
del(x);
}
}
return 0;
}
例题选讲
文艺平衡树
请写出一个可以翻转区间的数据结构。
这道题不能用传统的线段树来维护权值,而是要建一棵按照下标平衡的二叉树,每个结点存储下标对应的值,同时利用 \(Splay\) 的性质来调整结点顺序。文艺平衡树利用 \(lazy\) 思想,\(lazy\) 标记定义为当前结点的子树对应的区间是否需要修改。
显然,无论我们如何旋转,最终按照中序遍历都会依次遍历下标为 \(1\) 的值,下标为 \(2\) 的值……下标为 \(n\) 的值。所以,我们可以交换下标对应的值,从而达到区间翻转的效果。
当我们要旋转区间 \([l, r]\) 的时候,我们需要找到 \(l - 1\) 和 \(r + 1\) 对应的结点,并将 \(l - 1\) 对应的结点伸展到根结点,将 \(r + 1\) 对应的结点旋转成 \(l - 1\) 的右儿子。此时,\(r + 1\) 的左子树一定包含区间 \([l, r]\) 对应的结点。令 \(r + 1\) 的左儿子为 \(k\) ,此时给 \(k\) 打上 \(lazy\) 标记,表示区间 \([l, r]\) 需要翻转,并交换 \(k\) 的左右子树。
此外,文艺平衡树需要注意建树。我们并不需要一个个插入编号,只需要像线段树一样建树即可。在写文艺平衡树时,我们还要额外加入两个权值分别为 \(-\infty\) 和 \(\infty\) 的结点。这是为了翻转区间 \([1, n]\) 。每次可能导致左右儿子发生变化的时候,都要先下传 \(lazy\) 标记。最终输出时,我们只需要中序遍历一遍文艺平衡树就能求出最终的区间。
具体思路详见代码,如果看完本文您对文艺平衡树仍然有些小小的疑问,请前往 这篇博文 最后的例题选讲部分对文艺平衡树进行进一步的认识。
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 5;
const int inf = 0x3f3f3f3f;
int n, m, root, tot;
int a[maxn], val[maxn], fa[maxn], lazy[maxn];
int son[maxn][2], size[maxn];
bool get(int x) {
return son[fa[x]][1] == x;
}
void update(int x) {
if (x) {
size[x] = 1;
if (son[x][0]) {
size[x] += size[son[x][0]];
}
if (son[x][1]) {
size[x] += size[son[x][1]];
}
}
}
void push_down(int x) {
if (x && lazy[x]) {
lazy[son[x][0]] ^= 1;
lazy[son[x][1]] ^= 1;
swap(son[x][0], son[x][1]);
lazy[x] = 0;
}
}
void rotate(int x) {
int y = fa[x], z = fa[y], k = get(x);
son[y][k] = son[x][k ^ 1];
fa[son[y][k]] = y;
son[x][k ^ 1] = y;
fa[y] = x;
fa[x] = z;
if (z) {
son[z][son[z][1] == y] = x;
}
update(y);
update(x);
}
void splay(int x, int goal) {
for (int f; (f = fa[x]) != goal; rotate(x)) {
if (fa[f] != goal) {
rotate(get(f) == get(x) ? f : x);
}
}
if (goal == 0) {
root = x;
}
}
int build(int l, int r, int f) {
if (l > r) {
return 0;
}
int mid = (l + r) / 2;
int now = ++tot;
fa[now] = f;
son[now][0] = son[now][1] = 0;
val[now] = a[mid];
size[now] = 1;
son[now][0] = build(l, mid - 1, now);
son[now][1] = build(mid + 1, r, now);
update(now);
return now;
}
int find(int x) {
int now = root;
while (true) {
push_down(now);
if (x <= size[son[now][0]]) {
now = son[now][0];
} else {
x -= (size[son[now][0]] + 1);
if (!x) {
return now;
}
now = son[now][1];
}
}
}
void reverse(int x, int y) {
int l = x - 1, r = y + 1;
l = find(l);
r = find(r);
splay(l, 0);
splay(r, l);
int now = son[root][1];
now = son[now][0];
lazy[now] ^= 1;
}
void dfs(int now) {
push_down(now);
if (son[now][0]) {
dfs(son[now][0]);
}
if (val[now] != inf && val[now] != -inf) {
printf("%d ", val[now]);
}
if (son[now][1]) {
dfs(son[now][1]);
}
}
int main() {
int l, r;
scanf("%d%d", &n, &m);
a[1] = -inf;
for (int i = 1; i <= n; i++) {
a[i + 1] = i;
}
a[n + 2] = inf;
root = build(1, n + 2, 0);
for (int i = 1; i <= m; i++) {
scanf("%d%d", &l, &r);
reverse(l + 1, r + 1);
}
dfs(root);
return 0;
}
区间插入问题
给定一个长度为 \(n\) 的序列 \(a\) 和 \(m\) 次操作,每次操作可以:
-
把数 \(s\) 调整到序列开头
-
把数 \(s\) 调整到序列结尾
-
把数 \(s\) 向右移动 \(t\) 位
-
查询数 \(s\) 前的数值数量
-
查询序列中第 \(s\) 个数
显然,这道题需要维护区间,又需要在 \(O(log n)\) 的时间复杂度内进行单次修改,可以想到使用区间建树的 \(Splay\) 来维护。
实际上,前三种操作的本质都是一样的。这几种操作都需要先在文艺平衡树中删除 \(s\) ,然后再分别在 \(2, n + 1\) 和 \(s\) 原本的位置 \(+ t\) 处重新插入 \(s\) 。考虑在文艺平衡树中维护结点对应的下标和数值对应的结点,在代码中分别使用 val
和 pos
来表示。
假如我们需要在序列 \(k\) 的位置插入数值为 \(k\) 的结点,那么我们可以先找出文艺平衡树中下标为 \(x\) 和 \(x - 1\) 的结点,并把 \(x\) 结点旋转到根,\(x - 1\) 结点旋转成 \(x\) 的左儿子。此时 \(x - 1\) 一定没有右儿子,并且 \(k\) 结点刚好可以插入在 \(x - 1\) 的右儿子处。此时在 \(x - 1\) 的右儿子处新建结点,之后相应地更新结点信息就可以了。
接着考虑如何维护第四种操作。第四种操作实际上是询问文艺平衡树中 \(s\) 对应的结点的左子树大小 \(- 1\) 。我们可以直接找到 \(s\) 对应的结点,并把它旋转到根,最后返回根结点的左子树大小 \(- 1\) 即可。难点在于,文艺平衡树是 按下标平衡 的,也就是说,我们无法利用二叉查找树的性质找到 \(s\) 对应的结点。此时可以使用 pos
数组,方便快捷地找到数值对应的结点。
第五种操作实际上是询问文艺平衡树中排名为 \(s + 1\) 的结点的值,直接按照普通 \(Splay\) 的 find
函数修改即可。
#include <cstdio>
using namespace std;
const int maxn = 2e6 + 5;
int n, m, root, tot;
int a[maxn], size[maxn], son[maxn][2];
int fa[maxn], val[maxn], pos[maxn], cnt[maxn];
char opt[maxn];
void clear(int x)
{
size[x] = son[x][0] = son[x][1] = fa[x] = cnt[x] = 0;
pos[val[x]] = 0, val[x] = 0;
}
bool get(int x)
{
return son[fa[x]][1] == x;
}
void push_up(int x)
{
if (x)
{
size[x] = cnt[x];
if (son[x][0])
size[x] += size[son[x][0]];
if (son[x][1])
size[x] += size[son[x][1]];
}
}
void rotate(int x)
{
int y = fa[x], z = fa[y], k = get(x);
son[y][k] = son[x][k ^ 1];
fa[son[y][k]] = y;
son[x][k ^ 1] = y;
fa[y] = x;
fa[x] = z;
if (z)
son[z][son[z][1] == y] = x;
push_up(y);
push_up(x);
}
void splay(int x, int goal)
{
for (int f; (f = fa[x]) != goal; rotate(x))
if (fa[f] != goal)
rotate(get(x) == get(f) ? f : x);
if (!goal)
root = x;
}
int build(int l, int r, int f)
{
if (l > r)
return 0;
int mid = (l + r) / 2, u = ++tot;
fa[u] = f;
size[u] = cnt[u] = 1;
val[u] = a[mid];
pos[a[mid]] = u;
son[u][0] = build(l, mid - 1, u);
son[u][1] = build(mid + 1, r, u);
push_up(u);
return u;
}
int pre()
{
int u = son[root][0];
while (son[u][1])
u = son[u][1];
return u;
}
int rank(int x)
{
splay(pos[x], 0);
return size[son[root][0]] + 1;
}
int find(int x)
{
int u = root;
while (true)
{
if (son[u][0] && x <= size[son[u][0]])
u = son[u][0];
else
{
int sz = (son[u][0] ? size[son[u][0]] : 0) + cnt[u];
if (x <= sz)
return u;
x -= sz;
u = son[u][1];
}
}
}
void del(int x)
{
rank(x);
if (cnt[root] > 1)
{
cnt[root]--;
push_up(root);
return;
}
else if (!son[root][0] && !son[root][1])
{
clear(root);
root = 0;
return;
}
else if (!son[root][1])
{
int rt = root;
root = son[root][0];
fa[root] = 0;
clear(rt);
return;
}
else if (!son[root][0])
{
int rt = root;
root = son[root][1];
fa[root] = 0;
clear(rt);
return;
}
else
{
int p = pre(), rt = root;
splay(p, 0);
son[root][1] = son[rt][1];
fa[son[root][1]] = root;
clear(rt);
push_up(root);
return;
}
}
void update(int idx, int value)
{
int x = find(idx), y = find(idx - 1);
splay(x, 0);
splay(y, x);
son[y][1] = ++tot;
fa[tot] = y;
size[tot] = cnt[tot] = 1;
son[tot][0] = son[tot][1] = 0;
val[tot] = value;
pos[value] = tot;
push_up(y);
push_up(x);
}
int main()
{
int s, t, x;
scanf("%d%d", &n, &m);
a[1] = 0;
for (int i = 1; i <= n; i++)
scanf("%d", &a[i + 1]);
a[n + 2] = n + 1;
root = build(1, n + 2, 0);
for (int i = 1; i <= m; i++)
{
scanf("%s", opt);
if (opt[0] == 'T')
{
scanf("%d", &s);
del(s);
update(2, s);
}
else if (opt[0] == 'B')
{
scanf("%d", &s);
del(s);
update(n + 1, s);
}
else if (opt[0] == 'I')
{
scanf("%d%d", &s, &t);
x = rank(s);
del(s);
update(x + t, s);
}
else if (opt[0] == 'A')
{
scanf("%d", &s);
printf("%d\n", rank(s) - 2);
}
else
{
scanf("%d", &s);
printf("%d\n", val[find(s + 1)]);
}
}
return 0;
}