Splay
Splay
Basis
Definition
struct Miaksa
{
int root, tot;
struct SplayTree
{
int son[2]; // 左右子节点编号。0表示左子节点,1表示右子节点。
int val; // 该点权值
int sz; // 子树大小
int cnt; // 相同权值的数的个数
int fath; // 父节点编号
}tr[N];
}mks;
Make new
int make_new(int v)
{
tot++;
tr[tot].val = v;
tr[tot].sz = 1;
tr[tot].cnt = 1;
tr[tot].son[0] = tr[tot].son[1] = 0;
return tot;
}
Push up
void pushup(int p)
{
tr[p].sz = tr[tr[p].son[0]].sz + tr[tr[p].son[1]].sz + tr[p].cnt;
// 当前节点的子树大小 = 左子树大小 + 右子树大小 + 当前节点对应权值的数量
}
Rotate
void rotate(int x) // 旋转。下面均为白话文。
{
int y = tr[x].fath;
int z = tr[y].fath;
int a = tr[y].son[1] == x;
int b = tr[z].son[1] == y;
tr[z].son[b] = x;
tr[x].fath = z;
tr[y].son[a] = tr[x].son[a ^ 1];
tr[tr[x].son[a ^ 1]].fath = y;
tr[x].son[a ^ 1] = y;
tr[y].fath = x;
pushup(y); // 注意 pushup 的顺序!
pushup(x);
}
Splay
void splay(int x, int goal) // 将 x 旋到目标节点 goal 的子节点处。
{
while(tr[x].fath != goal)
{
int y = tr[x].fath;
int z = tr[y].fath;
if(z != goal) // 如果 x 的祖父 z 不是 goal,则一次循环内可以进行两次 rotate 操作,使 x 爬升两层
(tr[z].son[0] == y) ^ (tr[y].son[0] == x) ? rotate(x) : rotate(y);
// 根据 x, y, z 是否共线分为两种旋转方式,是为了保证正确的时间复杂度
rotate(x); // 一次循环内必然要将 x 旋上去一次
}
if(!goal) root = x; // 将 x 旋到根节点
}
Find
这是一个非常重要且易错的函数。
inline void find(int x) // 找到权值为 x 的节点,并旋到根节点(如果找不到,会操作其前驱或后继)
{
int u = root;
if(!u) return; // 空树
// 之后的函数中频繁用 x > tr[u].val 判断跳左儿子还是右儿子,十分方便
while(tr[u].son[x > tr[u].val] && x != tr[u].val)
u = tr[u].son[x > tr[u].val];
//找到了权值 x 后 u 会跳到对应的节点,找不到权值 x 则 u 会跳到 x 的前驱或后继。
splay(u, 0); // 之后的几个函数,每次调用后,都要向这样做一次正确复杂度的保证
}
Insert
inline void insert(int x) // 插入一个权值为 x 的数,并把对应节点旋到根节点
{
int u = root, fa = 0; // fa 用于平衡树中没有权值 x 的情况时,将新建节点与平衡树上的节点 fa 相连
while(u && tr[u].val != x)
{
fa = u; // u 往下跳,同时记录下一步跳到的 u 的父节点 fa
u = tr[u].son[x > tr[u].val];
}
if(u) tr[u].cnt++; // 找到了权值为 x 的节点,增加数量
else // 平衡树上没有权值为 x 的节点
{
u = make_new(x);
if(fa)
tr[fa].son[x > tr[fa].val] = u;
tr[u].fath = fa;
}
splay(u, 0);
}
Get the last
inline int get_lst(int x) // 查找权值 x 的前驱节点。前驱定义为最大的 < x 的数。
{
find(x);
int u = root;
if(tr[u].val < x) // find(x) 恰好跳到了前驱
return u;
u = tr[u].son[0]; // 在 < tr[u].val 的值域中查找最大的权值
//此时 tr[u].val >= x,则 u 的左子树中的任意节点权值一定 < x。(tr[u].val > x 时说明树中没有权值 x)
while(tr[u].son[1]) // 一直向右跳即可找到最值
u = tr[u].son[1];
return u;
}
Get the next
inline int get_nxt(int x) // 查找权值 x 的后继节点,原理与 get_lst 相同
{
find(x);
int u = root;
if(tr[u].val > x)
return u;
u = tr[u].son[1];
while(tr[u].son[0])
u = tr[u].son[0];
return u;
}
题外话:
对于前驱和后继的查询,笔者还尝试了这样一种方法:
inline int get_lst(int x)
{
int u = root, res = 0;
while(u)
{
if(tr[u].val < x) res = u;
u = tr[u].son[tr[u].val < x];
}
splay(res, 0);
return res;
}
inline int get_nxt(int x)
{
int u = root, res = 0;
while(u)
{
if(tr[u].val > x) res = u;
u = tr[u].son[tr[u].val <= x];
}
splay(res, 0);
return res;
}
其原理是从根向下跳,在不确定的探索中尽可能地靠近 x,并用途中满足条件的节点更新 res。
由于每次都尝试向 x 靠近,因此新的满足条件的节点一定更优。
最后将 res 旋到根处。
这样的做法是答案正确的,但是时间复杂度 可能 有问题。(是可以过模板的)(下面可能在说胡话)
问题就在于,我们一直搜到底,却只会 splay 从 res 往上的部分。
res 可能只是中间的某个节点:因此不能保证时间复杂度。
Remove
inline void remove(int x) // 删除一个权值为 x 的数
{
int lst = get_lst(x); // x 的前驱节点
int nxt = get_nxt(x); // x 的后继节点
splay(lst, 0); // 把 lst 作为根节点
splay(nxt, lst); // 将 nxt 作为 lst 的父节点,则此时 nxt 一定为 lst 的右节点。
// 由于 x > tr[lst].val,所以 x 在 nxt 子树内;
//又因为 x 是 nxt 子树范围内唯一可以 < tr[nxt].val 的数,因此权值 x 对应节点一定是 nxt 的左子节点
int u = tr[nxt].son[0]; // 找到权值 x 对应节点 u
if(tr[u].cnt > 1)
{
tr[u].cnt--;
splay(u, 0);
}
else // 权值数量为 1
tr[nxt].son[0] = 0; // 直接删去该节点。由之前的分析还可知,u 一定是叶节点,所以删除时只需要修改其父节点 nxt 的信息。
}
Get rank by value
权值 \(x\) 的排名定义为比 \(x\) 小的数的个数 \(+1\)。
这也是个初学时易错的函数。
inline int get_rank_by_val(int x) // 查询权值 x 的排名
{
find(x);
return tr[tr[root].son[0]].sz + (tr[root].val < x ? tr[root].cnt : 0) + 1;
}
也可以先插入 x 后再查询:
inline int get_rank_by_val(int x)
{
insert(x);
find(x);
int res = tr[tr[root].son[0]].sz + 1;
remove(x);
return res;
}
Get value by rank
inline int get_val_by_rank(int x)
{
int u = root;
if(tr[u].sz < x) // 排名超出了数的个数,找不到
return 0;
while(1) // 写过线段树二分或其他平衡树的话,这里就很好理解了
{
int v = tr[u].son[0];
if(x > tr[v].sz + tr[u].cnt)
{
x -= tr[v].sz + tr[u].cnt;
u = tr[u].son[1];
}
else
{
if(tr[v].sz >= x)
u = v;
else return tr[u].val;
}
}
}
Final Code
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6 + 5;
const int INF = INT_MAX;
struct Mikasa
{
int root, tot;
struct SplayTree
{
int son[2];
int val;
int sz, cnt;
int fath;
}tr[N];
int make_new(int v)
{
tot++;
tr[tot].val = v;
tr[tot].sz = 1;
tr[tot].cnt = 1;
tr[tot].son[0] = tr[tot].son[1] = 0;
return tot;
}
void pushup(int p)
{
tr[p].sz = tr[tr[p].son[0]].sz + tr[tr[p].son[1]].sz + tr[p].cnt;
}
void rotate(int x)
{
int y = tr[x].fath;
int z = tr[y].fath;
int a = tr[y].son[1] == x;
int b = tr[z].son[1] == y;
tr[z].son[b] = x;
tr[x].fath = z;
tr[y].son[a] = tr[x].son[a ^ 1];
tr[tr[x].son[a ^ 1]].fath = y;
tr[x].son[a ^ 1] = y;
tr[y].fath = x;
pushup(y), pushup(x);
}
void splay(int x, int goal)
{
while(tr[x].fath != goal)
{
int y = tr[x].fath;
int z = tr[y].fath;
if(z != goal)
(tr[z].son[0] == y) ^ (tr[y].son[0] == x) ? rotate(x) : rotate(y);
rotate(x);
}
if(!goal) root = x;
}
inline void find(int x)
{
int u = root;
if(!u) return;
while(tr[u].son[x > tr[u].val] && x != tr[u].val)
u = tr[u].son[x > tr[u].val];
splay(u, 0);
}
inline void insert(int x)
{
int u = root, fa = 0;
while(u && tr[u].val != x)
{
fa = u;
u = tr[u].son[x > tr[u].val];
}
if(u) tr[u].cnt++;
else
{
u = make_new(x);
if(fa)
tr[fa].son[x > tr[fa].val] = u;
tr[u].fath = fa;
}
splay(u, 0);
}
inline int get_lst(int x)
{
find(x);
int u = root;
if(tr[u].val < x)
return u;
u = tr[u].son[0];
while(tr[u].son[1])
u = tr[u].son[1];
splay(u, 0);
return u;
}
inline int get_nxt(int x)
{
find(x);
int u = root;
if(tr[u].val > x)
return u;
u = tr[u].son[1];
while(tr[u].son[0])
u = tr[u].son[0];
splay(u, 0);
return u;
}
inline void remove(int x)
{
int lst = get_lst(x);
int nxt = get_nxt(x);
splay(lst, 0);
splay(nxt, lst);
int u = tr[nxt].son[0];
if(tr[u].cnt > 1)
{
tr[u].cnt--;
splay(u, 0);
}
else
tr[nxt].son[0] = 0;
}
inline int get_rank_by_val(int x)
{
find(x);
return tr[tr[root].son[0]].sz + (tr[root].val < x ? tr[root].cnt : 0) + 1;;
}
inline int get_val_by_rank(int x)
{
int u = root;
if(tr[u].sz < x)
return 0;
while(1)
{
int v = tr[u].son[0];
if(x > tr[v].sz + tr[u].cnt)
{
x -= tr[v].sz + tr[u].cnt;
u = tr[u].son[1];
}
else
{
if(x <= tr[v].sz)
u = v;
else
{
splay(u, 0);
return tr[u].val;
}
}
}
}
}mks;
int n, Q, lastans, ans;
int main()
{
mks.insert(INF);
mks.insert(-INF);
scanf("%d %d", &n, &Q);
for(int i = 1; i <= n; ++i)
{
int x; scanf("%d", &x);
mks.insert(x);
}
while(Q--)
{
int op, x;
scanf("%d %d", &op, &x);
x ^= lastans;
if(op == 1)
mks.insert(x);
if(op == 2)
mks.remove(x);
if(op == 3)
lastans = mks.get_rank_by_val(x) - 1;
if(op == 4)
lastans = mks.get_val_by_rank(x + 1);
if(op == 5)
lastans = mks.tr[mks.get_lst(x)].val;
if(op == 6)
lastans = mks.tr[mks.get_nxt(x)].val;
if(op > 2) ans ^= lastans;
}
printf("%d\n", ans);
return 0;
}
Time Complexity
这一部分感谢 cool_milo 的笔记对我的帮助。
由于 splay 操作是对其他函数的时间复杂度的保证,因此我们需先分析 splay 函数的时间复杂度。
设旋转操作 rotate 的复杂度为 \(k\),\(sz_x\) 为以 \(x\) 为根的子树大小,势能函数 \(\phi(x) = k\log{sz_x}\)。这里及以下的 \(\log\) 运算均以 \(2\) 为底。记 \(\Phi = \sum{\phi_x}\)。注意势能函数是我们人为定义的,因此为了更好地分析复杂度,有必要有一个 \(k\) 的倍数。
对于一棵不平衡的树,\(\Phi\) 会变大。
我们需要计算 \(\Delta{\Phi}\),定义 \(\Delta{\Phi}\) 表示 rotate 操作导致的 势能变化与所用时间的总和。记 \(\phi^{'}(x)\) 为 rotate 后的势能,\(sz^{'}_x\) 为 rotate 后的子树大小。
rotate 要分三种情况。现有节点 \(x\),\(y\) 为 \(x\) 的父节点,\(z\) 为 \(y\) 的父节点。
\(A, B, C, D\) 均表示子树而非节点。
rotate(x)
与 \(z\) 没有关系。


rotate(x), rotate(x)
注意看首尾两个状态,第二张图是过程。

通过分式比较大小来放缩,似乎效果不是很好(其实是我不会后面怎么推了)。
不妨不用势能函数的原始定义,而是根据 \(\phi(x)\) 关于 \(sz_x\) 单增的性质放缩。(\(\phi^{'}(x)\) 同理)
由 \(sz_{x} < sz_{y}\),所以 \(\phi(x) < \phi(y)\),所以由上式的第二步可以直接得出:
而 \(\phi^{'}(y) < \phi^{'}(x)\),所以后面一种放缩方式更优。
Lemma 1:\(\phi^{'}(y) + \phi^{'}(z) - 2\phi^{'}(x) \le -1\)。
证:
由引理 \(1\),
将原式加上一个非负数的结果不小于其本身,利用这一点进行放缩,即:
rotate(y), rotate(x)
注意看首尾两个状态,第二张图是过程。

Lemma 2:\(\phi(x) + \phi^{'}(z) - 2\phi^{'}(x) \le -1\)。
证:
同样的,将原式加上非负数 $-1 + 2\phi^{'}(x) - \phi^{'}(z) - \phi(x) $ 以进行放缩:
然后分析 splay 的时间复杂度。对于从 \(x\) 旋到根 \(root\) 的过程,我们记 \(\Phi\) 的总变化量为 \(W\)。
对于双旋,取 \(\Delta{\Phi} \le \max\left( 2\left( \phi^{'}(x) - \phi(x) \right), 3\left( \phi^{'}(x) - \phi(x) \right) \right) = 3\left( \phi^{'}(x) - \phi(x) \right)\),总计贡献 \(3\left( \phi(root) - \phi(x) \right) \le 3k\log{n}\)。
对于单旋,每次 splay 操作中至多执行一次,所以会贡献 \(k + \Delta{\phi} \le k + k\log{n}\)。并且后面的 \(k\log{n}\) 抵满的几率很小。
我们将势能变化量与时间消耗合起来考虑为 \(W\),但我们想要知道时间消耗是多少,就必须把势能变化量减掉。
考虑链和完全二叉树两种极端情况。 \(\Phi_{max} = k\sum\limits_{i = 1}^{n}\log{i} = O(n\log{n})\),\(\Phi_{min} = k\sum\limits_{i = 1}^{\log{n}}\log{(2^i\times i)} = k\sum\limits_{i = 1}^{\log{n}}i\log{i}\)。
不会算
事实上,考虑单次 splay 操作也并不是一件明智的事情。我们再扩大考虑范围,考虑多次 splay 的总时间复杂度,此时总势能变化量的数量级是 \(O(n\log{n})\) 的,假设操作次数 \(Q\) 与 \(n\) 同级,那么 \(W\) 的总和也是 \(O(n\log{n})\) 的,于是多次 splay 的总时间复杂度为 \(O(n\log{n})\)。
考虑 splay 操作中层数与 rotate 的操作次数 \(t\) 是同级的,而操作次数 \(t\) 可以与 splay 的总时间复杂度相关联,而 rotate 的复杂度是 \(O(1)\) 的,可得 \(t\) 的数量级是 \(O(n\log{n})\),均摊到 \(Q\) 次询问,树的平均深度处于 \(O(\log{n})\) 级别,则其他函数的均摊时间复杂度也可以确定在 \(O(\log{n})\) 级别。由此分析,特别注意:对于那些从根节点往下跳的操作,每次到达最终节点 \(u\) 后都要进行 splay(u, 0)。
文艺平衡树
考察区间打标记。
考虑用下标作为在平衡树上排序的权值,此时排名为 k 的数即为序列中的第 k 个数。
注意这样一件事:插入到平衡树上的权值仍然为原序列中的权值,但我们查到的排名需要是实时更新的排名,即原序列中的下标。
但最初插入到平衡树上的数已经是有了一定顺序的,这个时候有两种处理方法:
-
把原序列的排名和对应权值一起插进平衡树,用排名排序,构建出最初形态的平衡树;
此后,原序列排名被弃用,之后序列的顺序由平衡树的形态不断变化而随之变化。
-
中序遍历直接建树。
那么显然是第二种方法更优。
另外,这里可能存在权值相同的数,我们 不能 将它们合并到一个节点,即每个节点的 cnt 均为 \(1\),所以可以不用记录这个量。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
const int INF = INT_MAX;
int n, Q, a[N];
struct Mikasa
{
int root, tot;
struct Nagisa
{
int rev;
};
struct SplayTree
{
int son[2];
int val;
int sz;
int fath;
Nagisa dango;
}tr[N];
void pushup(int p)
{
tr[p].sz = tr[tr[p].son[0]].sz + tr[tr[p].son[1]].sz + 1;
}
int build(int l, int r, int fa) // 类比替罪羊树的重构,注意替罪羊树是对节点 cur[mid] 的信息处理
{
if(l > r) return 0;
int mid = l + (r - l) / 2;
tr[mid].val = a[mid];
tr[mid].fath = fa;
tr[mid].sz = 1;
tr[mid].son[0] = build(l, mid - 1, mid);
tr[mid].son[1] = build(mid + 1, r, mid);
pushup(mid);
return mid;
}
void pushdown(int p)
{
if(tr[p].dango.rev)
{
tr[tr[p].son[0]].dango.rev ^= 1;
tr[tr[p].son[1]].dango.rev ^= 1;
swap(tr[p].son[0], tr[p].son[1]);
tr[p].dango.rev = 0;
}
}
void rotate(int x)
{
int y = tr[x].fath;
int z = tr[y].fath;
int a = tr[y].son[1] == x;
int b = tr[z].son[1] == y;
tr[z].son[b] = x;
tr[x].fath = z;
tr[y].son[a] = tr[x].son[a ^ 1];
tr[tr[x].son[a ^ 1]].fath = y;
tr[x].son[a ^ 1] = y;
tr[y].fath = x;
pushup(y), pushup(x);
}
void splay(int x, int goal)
{
while(tr[x].fath != goal)
{
int y = tr[x].fath;
int z = tr[y].fath;
if(z != goal)
(tr[z].son[0] == y) ^ (tr[y].son[0] == x) ? rotate(x) : rotate(y);
rotate(x);
}
if(!goal) root = x;
}
inline int get_kth(int u, int x) // 注意返回的是排名为 k 的节点而非权值
{
pushdown(u);
if(x > tr[tr[u].son[0]].sz && x <= tr[tr[u].son[0]].sz + 1)
{ splay(u, 0); return u; }
if(x <= tr[tr[u].son[0]].sz)
return get_kth(tr[u].son[0], x);
return get_kth(tr[u].son[1], x - tr[tr[u].son[0]].sz - 1);
}
inline void reverse(int l, int r) // actually [l + 1, r + 1]
{
l = get_kth(root, l);
r = get_kth(root, r + 2);
splay(l, 0);
splay(r, l);
tr[tr[tr[l].son[1]].son[0]].dango.rev ^= 1;
}
void print(int u)
{
if(!u) return;
pushdown(u);
print(tr[u].son[0]);
if(tr[u].val != INF)
printf("%d ", tr[u].val);
print(tr[u].son[1]);
}
}mks;
int main()
{
scanf("%d %d", &n, &Q);
for(int i = 2; i <= n + 1; ++i)
a[i] = i - 1;
a[1] = a[n + 2] = INF;
mks.root = mks.build(1, n + 2, 0);
while(Q--)
{
int l, r;
scanf("%d %d", &l, &r);
mks.reverse(l, r);
}
mks.print(mks.root);
return 0;
}

浙公网安备 33010602011771号