Splay

伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操作。它由丹尼尔·斯立特Daniel Sleator 和 罗伯特·恩卓·塔扬Robert Endre Tarjan 在1985年发明的
在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置。于是想到设计一个简单方法, 在每次查找之后对树进行重构,把被查找的条目搬移到离树根近一些的地方。伸展树应运而生。伸展树是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。

 

1、时间局部性和空间局部性的原理

(1)刚刚被访问的元素,极有可能在不久后再次被访问

(2)刚刚被访问的元素,它的相邻节点也很有可能被访问

 

伸展树的实现更为便捷,它无须时刻保持全树平衡,任意节点的左右子树高差无限制。伸展树的单次搜索也可能需要n次操作,但可以在任意足够长的真实操作序列中保持均摊意义上的高效率。伸展树可以保证m次连续搜索操作的复杂度为,而不是。伸展树的优势在于不需要记录平衡因子、树高、子树大小等额外信息,所以适用范围更广,对m次连续搜索操作具有较高的效率。

考虑到局部性原理,伸展树会在每次操作后都将被访问的节点旋转至树根,加速后续的操作。当然,旋转前后的搜索树必须相互等价。这样,查询频率高的节点应当经常处于靠近树根的位置。旋转的巧妙之处在于不打乱数列中数据大小的关系(中序有序性)的情况下,所有操作的均摊复杂度仍为O(mlogn)。

 

下面一起来看看Splay是如何实现的吧!

 

首先要熟悉的是左旋和右旋操作,普通treap中是左旋和右旋分开来的

分别是右旋zig

void zig(int &p)
{
    int q = tr[p].l;
    tr[p].l = tr[q].r, tr[q].r = p, p = q;
    pushup(tr[p].r), pushup(p);
}

左旋zag

void zag(int &p)
{
    int q = tr[p].l;
    tr[p].l = tr[q].r, tr[q].r = p, p = q;
    pushup(tr[p].r), pushup(p);
}

经过这些年大佬们的努力,把左右旋操作并为一个函数

左右旋rotate(最好自己画图理解一下)

void rotate(int x)
{
    int y = tr[x].p, z = tr[y].p;//父亲节点
    int k = tr[y].s[1] == x; // 判断x是y的左儿子还是右儿子,如果是左儿子返回0,右儿子返回1
    tr[z].s[tr[z].s[1] == y] = x, tr[x].p = z; //同上
    tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y;
    tr[x].s[k ^ 1] = y, tr[y].p = x;
}

关键函数Splay

复制代码
void splay(int x, int k) //将x旋转到节点k的下面  (注意千万不能改变旋转的顺序,否则不能保证logn的复杂度)
{
    while(tr[x].p != k) // 如果x的父节点不是k,继续旋转
    {
        int y = tr[x].p, z = tr[y].p;
        if(z != k)//如果z不是k的话,就旋转两次,否则旋转一次
            if((tr[z].s[1] == y) ^ (tr[y].s[1] == x)) rotate(x); //如果是折线的话旋转两次x
            else rotate(y);//否则就旋转y,再旋转x
        rotate(x);
    }
    if(!k) root = x; //如果k等于0,x是根节点
}
复制代码

 

模板题:https://www.acwing.com/problem/content/2439/

代码展示:

复制代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 100010;
int n, m;

struct Node{
    int s[2], val, p;
    int sz, lazy;
    
    void init(int _v, int _p){
        val = _v;
        p = _p;
    }
}tr[N];
int root, idx;

void pushup(int u)
{
    tr[u].sz = tr[tr[u].s[0]].sz + tr[tr[u].s[1]].sz + 1;
}

void pushdown(int u)
{
    if(tr[u].lazy)
    {
        swap(tr[u].s[0], tr[u].s[1]);
        tr[tr[u].s[0]].lazy ^= 1;
        tr[tr[u].s[1]].lazy ^= 1;
        tr[u].lazy = 0;
    }
}

void rotate(int x)
{
    int y = tr[x].p, z = tr[y].p;
    int k = tr[y].s[1] == x;
    tr[z].s[tr[z].s[1] == y] = x, tr[x].p = z;
    tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y;
    tr[x].s[k ^ 1] = y, tr[y].p = x;
    pushup(y), pushup(x);
}

void splay(int x, int k)
{
    while (tr[x].p != k)
    {
        int y = tr[x].p, z = tr[y].p;
        if (z != k)
            if ((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x);
            else rotate(y);
        rotate(x);
    }
    if (!k) root = x;
}

void insert(int v)
{
    int u = root, p = 0;
    while(u) p = u, u = tr[u].s[v > tr[u].val];
    
    u = ++ idx;
    if(p) tr[p].s[v > tr[p].val] = u;
    tr[u].init(v, p);
    splay(u, 0);
}

int get_k(int k)
{
    int u = root;
    while(true)
    {
        pushdown(u);
        if(tr[tr[u].s[0]].sz >= k) u = tr[u].s[0];
        else if(tr[tr[u].s[0]].sz + 1 == k) return u;
        else k -= tr[tr[u].s[0]].sz + 1, u = tr[u].s[1];
    }
    return -1;
}

void ldr(int u)
{
    if(!u) return;
    pushdown(u);
    ldr(tr[u].s[0]);
    if(tr[u].val >= 1 && tr[u].val <= n) printf("%d ", tr[u].val);
    ldr(tr[u].s[1]);
}

int main()
{
    cin >> n >> m;
    
    for(int i = 0; i <= n + 1; i ++ ) insert(i);
    
    while(m -- )
    {
        int l, r;
        cin >> l >> r;
        int L = get_k(l), R = get_k(r + 2);
        splay(L, 0), splay(R, L);
        tr[tr[R].s[0]].lazy ^= 1;
    }
    
    ldr(root);
    
    return 0;
}
复制代码
posted @   jay1573  阅读(111)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示