其实一开始学习伸展树的时候比较艰难(其实还是自己太菜了QAQ),一个原因是找不到可以入门的水题,还有一个原因是网上其他博客说了很多splay的原理,代码实现却没讲的特别多。然后自己算是小结了一个模板吧,就记在这里
我最开始参考的是cxlove的代码,然后自己改了改,想看他代码的可以直接百度一些splay的题,很多题目第一条结果就是他的博客
英文好的同学可以直接看一下wiki,23333:https://en.wikipedia.org/wiki/Splay_tree
或者这一篇博客也写的挺明白的(但是这篇博客的第四个图片是有错误的,关于zig-zig的那个,读者可以自己想想正确的图示应该是怎样的):http://blog.csdn.net/ric_shooter/article/details/20636487
自己大致说一下,就是这是一棵二叉排序树,旋转操作比较简便,所以它也没有固定的形状,最坏情况下的一次操作的时间复杂度是 O(n) ,但是平均一下还是有 O(logn) 的表现的,其次它易于手写吧,所以在比赛中也被大家使用。
基本的操作就是旋转,zag 左旋,zig 右旋,(在本篇博客中,这些操作由函数 Rotate(int x, int kind)
实现)。
其次,基于这些操作,就可以把树中的某一个节点移到我们需要的位置(这个操作由函数splay(int x, int goal)
实现,该函数根据不同的情况不断调用Rotate()
函数,直到实现需求)
原理大致就是这样吧(写的有点水,可毕竟代码实现更重要吧……)
首先是数据元素:
然后是函数:
一些定义:
因为我用的是codeblocks(正式比赛时也会有很多人用吧),不断的在数组下标中打数组下标是一件很累的事,所以我用lson()
和rson()
代替了
#define MS(x, y) memset(x, y, sizeof(x))
#define lson(x) son[x][0]
#define rson(x) son[x][1]
// 把l - 1移到根,把r + 1移到根的右节点,那么keyvalue指的就是[l, r]区间
#define keyvalue son[son[root][1]][0]
debug部分:(cxlove说这个copy自胡浩学长)
用的时候只要调用debug()
函数就好了,很多题目是操作模拟题,可以设置一个额外的操作为debug操作
void treaval(int x) {
if (!x) return ;
treaval(lson(x));
printf("结点%2d: 左儿子 %2d 右儿子 %2d 父节点 %2d size = %2d, val = %2d\n",
x, lson(x), rson(x), pre[x], siz[x], data[x]);
treaval(rson(x));
}
void debug() {
printf("root: %d\n", root);
treaval(root);
}
零散的push部分(就像线段树的pushup()
和pushdown()
)
void pushup(int x) {
siz[x] = siz[lson(x)] + siz[rson(x)] + 1;
}
void pushdown(int x) {
if (rev[x]) {
rev[lson(x)] ^= true;
rev[rson(x)] ^= true;
swap(son[lson(x)][0], son[lson(x)][1]);
swap(son[rson(x)][0], son[rson(x)][1]);
rev[x] = false;
}
if (add[x]) {
add[lson(x)] += add[x];
add[rson(x)] += add[x];
data[lson(x)] += add[x];
data[rson(x)] += add[x];
add[x] = 0;
}
}
// update keyvalue
void updkv() {
pushup(rson(root));
pushup(root);
}
初始化部分:
// 如果method为正数,就是对某一个指定节点进行初始化
int newnode(int val, int fa, int method = -1) {
int ret;
if (method > 0) ret = method;
else ret = ++tot;
pre[ret] = fa;
siz[ret] = 1; // 或者其他一些计算一个节点size的操作
num[ret] = 1; // 节点本身指代的值的大小,一般问题这个值都是1,有些问题还是会有差(比如问题要离散化之类的)
rev[ret] = false; // 题目没有该操作的话就不必要
add[ret] = 0; // 题目没有该操作的话就不必要
data[ret] = val;
node[val] = tot; // node与data互为逆运算,有时这个操作是不必要,或者是非法的
MS(son[ret], 0);
return ret;
}
// 这个过程就是把一个区间变为一棵树,树的中序遍历结果就是这个区间,具体题目具体分析
int build(int fa, int l, int r) {
if (l > r) return 0;
int m = (l + r) >> 1;
int rt = newnode(val(m), fa);
lson(tot) = build(tot, l, m - 1);
rson(tot) = build(tot, m + 1, r);
pushup(rt)
return rt;
}
// 每次初始化都把0号节点对应的信息清空
// 然后插入一头一尾两个节点,让这颗树永远不会变为空树
// 但是插入两个节点的操作会让很多步骤变得复杂,所以可以不加就不加(比如题目保证了树不会变空)
void init() {
tot = 0;
son[0][0] = son[0][1] = pre[0] = rev[0] = siz[0] = 0;
root = newnode(-1, 0);
son[root][1] = newnode(-1, root);
keyvalue = build(son[root][1], 1, n);
pushup(son[root][1]);
pushup(root);
}
splay部分
//1是zig, 0是zag
void Rotate(int x, int kind) {
int y = pre[x];
pushdown(y);
pushdown(x);
son[y][!kind] = son[x][kind];
pre[son[x][kind]] = y;
if (pre[y]) {
son[pre[y]][son[pre[y]][1] == y] = x;
}
pre[x] = pre[y];
son[x][kind] = y;
pre[y] = x;
pushup(y);
}
//把x变为goal的子节点
void splay(int x, int goal) {
pushdown(x);
while (pre[x] != goal) {
if (pre[pre[x]] == goal) Rotate(x, son[pre[x]][0] == x);
else {
int y = pre[x];
int kind = (son[pre[y]][0] == y);
if (son[y][kind] == x) {
Rotate(x, !kind);
Rotate(x, kind);
} else {
Rotate(y, kind);
Rotate(x, kind);
}
}
}
pushup(x);
if (goal == 0) root = x;
}
一些得到特定节点的操作
// 得到一棵树的最小值,得到最大值就是一直往右走
int get_min(int x) {
pushdown(x);
while (lson(x)) {
pushdown(lson(x));
x = lson(x);
}
return x;
}
int get_max(int x) {
pushdown(x);
while (rson(x)) {
pushdown(rson(x));
x = rson(x);
}
return x;
}
int get_kth(int k, int x) {
int s;
while (x) {
s = siz[lson(x)] + 1;
if (s == k) return x;
if (s > k) x = lson(x);
else k -= s, x = rson(x);
}
}
删除根节点的操作
注意!删节点一定要把这个节点先旋转到根节点再删(或者某一个离根很近的范围),因为在根节点删除的话,就只需要pushup(root)
一次,如果不是在根节点的话,就要不断pushup更新整棵splay树,代价和把节点旋转到根节点差不多,代码却比只删根的策略来的复杂,插入也差不多同理
// 删除根节点
void deleteroot() {
pushdown(root);
if (!lson(root) || !rson(root)) {
root = lson(root) + rson(root);
pre[root] = 0;
return ;
}
int k = get_min(rson(root));
splay(k, root);
lson(k) = lson(root);
pre[lson(root)] = k;
root = k;
pre[root] = 0;
pushup(root);
}
最后放几道我做过的题目,建议按这个顺序做一做
HDU 3436 题解
HDU 3487
HDU 4441
HDU 4453
HDU 1890
HDU 3726 题解