基本概念
\(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;
}