如何用 $\text{Splay}$ 维护二叉查找树
$\text{Splay}$ 是一种二叉查找树,它通过不断将某个结点旋转到根结点,使得整棵树仍然满足二叉查找树的性质,并且保持平衡而不至于退化为链,它由 Daniel Sleator 和 Robert Tarjan 发明。
首先肯定是一棵二叉树!
能够在这棵树上查找某个值的性质:左子树任意结点的值 $<$ 根结点的值 $<$ 右子树任意结点的值。
$rt$ | $tot$ | $fa[i]$ | $ch[i][0/1]$ | $val[i]$ | $cnt[i]$ | $sz[i]$ |
---|---|---|---|---|---|---|
根结点编号 | 结点个数 | 父亲 | 左右儿子编号 | 结点权值 | 权值出现次数 | 子树大小 |
void maintain(int x) { sz[x] = sz[ch[x][0]] + sz[ch[x][1]] + cnt[x]; } bool get(int x) { return x == ch[fa[x]][1]; } void clear(int x) { ch[x][0] = ch[x][1] = fa[x] = val[x] = sz[x] = cnt[x] = 0; }
为了使 $\text{Splay}$ 保持平衡而进行旋转操作,旋转的本质是将某个结点上移一个位置。
旋转需要保证:
在 $\text{Splay}$ 中旋转分为两种:左旋和右旋。
具体分析旋转步骤(假设需要旋转的结点为 $x$,其父亲为 $y$,以右旋为例)
ch[y][0]=ch[x][1];fa[ch[x][1]]=y;
ch[x][chk^1]=y;fa[y]=x;
fa[x]=z;if(z) ch[z][y==ch[z][1]]=x;
void rotate(int x) { int y = fa[x], z = fa[y], chk = get(x); ch[y][chk] = ch[x][chk ^ 1]; fa[ch[x][chk ^ 1]] = y; ch[x][chk ^ 1] = y; fa[y] = x; fa[x] = z; if (z) ch[z][y == ch[z][1]] = x; maintain(y); maintain(x); }
$\text{Splay}$ 规定:每访问一个结点后都要强制将其旋转到根结点。此时旋转操作具体分为 $6$ 种情况讨论(其中 $x$ 为需要旋转到根的结点)
分析起来一大串,其实代码一小段。大家可以自己模拟一下 $6$ 种旋转情况,就能理解 $\text{Splay}$ 的基本思想了。
void splay(int x) { for (int f = fa[x]; f = fa[x], f; rotate(x)) if (fa[f]) rotate(get(x) == get(f) ? f : x); rt = x; }
插入操作是一个比较复杂的过程,具体步骤如下(插入的值为 $k$):
void ins(int k) { if (!rt) { val[++tot] = k; cnt[tot]++; rt = tot; maintain(rt); return; } int cnr = rt, f = 0; while (1) { if (val[cnr] == k) { cnt[cnr]++; maintain(cnr); maintain(f); splay(cnr); break; } f = cnr; cnr = ch[cnr][val[cnr] < k]; if (!cnr) { val[++tot] = k; cnt[tot]++; fa[tot] = f; ch[f][val[f] < k] = tot; maintain(tot); maintain(f); splay(tot); break; } } }
根据二叉查找树的定义和性质,显然可以按照以下步骤查询 $x$ 的排名:
注意最后需要进行 $\text{Splay}$ 操作。
int rk(int k) { int res = 0, cnr = rt; while (1) { if (k < val[cnr]) { cnr = ch[cnr][0]; } else { res += sz[ch[cnr][0]]; if (k == val[cnr]) { splay(cnr); return res + 1; } res += cnt[cnr]; cnr = ch[cnr][1]; } } }
设 $k$ 为剩余排名,具体步骤如下:
int kth(int k) { int cnr = rt; while (1) { if (ch[cnr][0] && k <= sz[ch[cnr][0]]) { cnr = ch[cnr][0]; } else { k -= cnt[cnr] + sz[ch[cnr][0]]; if (k <= 0) { splay(cnr); return val[cnr]; } cnr = ch[cnr][1]; } } }
前驱定义为小于 $x$ 的最大的数,那么查询前驱可以转化为:将 $x$ 插入(此时 $x$ 已经在根的位置了),前驱即为 $x$ 的左子树中最右边的结点,最后将 $x$ 删除即可。
int pre() { int cnr = ch[rt][0]; while (ch[cnr][1]) cnr = ch[cnr][1]; splay(cnr); return cnr; }
后继定义为大于 $x$ 的最小的数,查询方法和前驱类似:$x$ 的右子树中最左边的结点。
int nxt() { int cnr = ch[rt][1]; while (ch[cnr][0]) cnr = ch[cnr][0]; splay(cnr); return cnr; }
合并两棵 Splay 树,设两棵树的根结点分别为 $x$ 和 $y$,那么我们要求 $x$ 树中的最大值小于 $y$ 树中的最小值。合并操作如下:
删除操作也是一个比较复杂的操作,具体步骤如下:
首先将 $x$ 旋转到根的位置。
void del(int k) { rk(k); if (cnt[rt] > 1) { cnt[rt]--; maintain(rt); return; } if (!ch[rt][0] && !ch[rt][1]) { clear(rt); rt = 0; return; } if (!ch[rt][0]) { int cnr = rt; rt = ch[rt][1]; fa[rt] = 0; clear(cnr); return; } if (!ch[rt][1]) { int cnr = rt; rt = ch[rt][0]; fa[rt] = 0; clear(cnr); return; } int cnr = rt, x = pre(); splay(x); fa[ch[cnr][1]] = x; ch[x][1] = ch[cnr][1]; clear(cnr); maintain(rt); }