splay 伸展树小结

9 篇文章 0 订阅
4 篇文章 0 订阅

其实一开始学习伸展树的时候比较艰难(其实还是自己太菜了QAQ),一个原因是找不到可以入门的水题,还有一个原因是网上其他博客说了很多splay的原理,代码实现却没讲的特别多。然后自己算是小结了一个模板吧,就记在这里
我最开始参考的是cxlove的代码,然后自己改了改,想看他代码的可以直接百度一些splay的题,很多题目第一条结果就是他的博客

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()函数,直到实现需求)

原理大致就是这样吧(写的有点水,可毕竟代码实现更重要吧……)

代码实现

首先是数据元素:

  • pre[] – 储存某点的父节点
  • son[][] – 储存某点的两个儿子
  • siz[] – 以某点为根节点的子树大小
  • num[] – 储存某点的值出现了几次
  • data[] – 某点的值
  • node[] – 某值的点
  • rev[] – lazy数组,记录以某点为根的子树有没有被翻转
  • add[] – lazy数组,记录以这个节点为根的子树有没有集体增加值的操作
    -
  • tot – 目前产生的点数
  • root – 根节点

然后是函数:

一些定义:
因为我用的是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 题解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值