学习笔记——splay

splay也是平衡树的一种,二叉搜索树的性质splay也都有

隶属于splay的特殊性质:

1.每次操作都要把被操作的点转到根上,以保证随机性

2.很难被卡

3.对于一棵树,你越splay,这棵树就越平衡

所以spaly应用范围很广

下面以文艺平衡树为例

  1 #include<iostream>
  2 #include<cstdio>
  3 using namespace std;
  4 #define MAXN 1000007
  5 #define INF 1<<30
  6 struct ziji{
  7     int f,sub_size,cnt,val,tag,son[2];
  8     //tag打反转标记 
  9     //s[i].sub_size代表以i为根的子树的大小
 10     //f是每个节点的父亲
 11     //cnt记录自己被重复统计了几次
 12     //son[1]右孩子,son[0]左孩子 
 13 }s[MAXN];
 14 int ori[MAXN],root,wz,n,m,x,y;
 15 //ori[i]是放置元素的储存值 
 16 inline bool which(int x){
 17     return x==s[s[x].f].son[1];
 18     //判断当前节点和爸爸关系 
 19     //右子树就return true else return false 
 20 }
 21 inline void update(int x){
 22     if(x){
 23         s[x].sub_size=s[x].cnt;
 24         if(s[x].son[0]) s[x].sub_size+=s[s[x].son[0]].sub_size;
 25         if(s[x].son[1]) s[x].sub_size+=s[s[x].son[1]].sub_size;
 26     }
 27     //更新这个节点的sub_size值
 28     //其实就是自己被提及的次数加上左右子树的大小 
 29 }
 30 inline void pushdown(int x){
 31     //我们在反转时,选择了以打标记的形式
 32     //所以在询问或者做某些改变父子关系时就要像线段树一样下放标记 
 33     //我么可以通过画图的形式得知,这棵树的中序遍历就是这个序列
 34     //所以反转序列就相当于把左右两棵子树反转 
 35     if(x&&s[x].tag){
 36         //如果这个点存在且这个点有标记 
 37         s[s[x].son[1]].tag^=1; 
 38         s[s[x].son[0]].tag^=1;
 39         //左右子树要反转,所以标记也要变 
 40         swap(s[x].son[1],s[x].son[0]);
 41         s[x].tag=0;//这个点标记下放完了 
 42     }
 43 } 
 44 inline void rotate(int x){//双旋 
 45     int fnow=s[x].f,ffnow=s[fnow].f;
 46     //存一下爸爸和爷爷 
 47     pushdown(x),pushdown(fnow);
 48     //父子关系变了,就要下放标记  
 49     bool w=which(x);//看父子间关系 
 50     s[fnow].son[w]=s[x].son[w^1];
 51     //0^1=1,1^1=0,0^0=1,所以w^1可以很好的转变子树关系 
 52     s[s[fnow].son[w]].f=fnow;
 53     s[fnow].f=x;s[x].f=ffnow;
 54     s[x].son[w^1]=fnow;
 55     //具体的旋转操作(一次) 
 56     if(ffnow) s[ffnow].son[s[ffnow].son[1]==fnow]=x;
 57     //如果有爷爷,并且爷爷的右孩子就是爸爸
 58     //那么x成为爷爷的右孩子,否则就是左孩子 
 59     update(fnow);//更新,维护性质 
 60 }
 61 inline void splay(int x,int goal){//伸展 
 62     for(register int qwq;(qwq=s[x].f)!=goal;rotate(x)){
 63         if(s[qwq].f!=goal)//如果x的爷爷不是我们想要的旋转目标 
 64             rotate(which(x)==which(qwq)?qwq:x);
 65             //就开始向上旋转
 66             //旋转规则:孩子和父亲在同一方向按孩子双旋
 67             //否则就按父亲双旋 
 68     }
 69     if(goal==0) root=x;//如果目标成为0了,即你已经转到整棵树的根了,你就停止 
 70 }
 71 inline int build_tree(int l,int r,int fa){
 72     if(l>r) return 0;//平常的建树 
 73     int mid=l+r>>1,now=++wz;
 74     s[now].f=fa;s[now].son[0]=s[now].son[1]=0;
 75     s[now].cnt++;s[now].val=ori[mid];s[now].sub_size++;
 76     s[now].son[1]=build_tree(mid+1,r,now);
 77     s[now].son[0]=build_tree(l,mid-1,now);
 78     update(now);return now; 
 79 }
 80 inline int find(int x){//按照排名找数值 
 81     int now=root;
 82     while(1){
 83         pushdown(now);//你要找的是现在的状态,所以要下放标记 
 84         if(x<=s[s[now].son[0]].sub_size) now=s[now].son[0]; 
 85         else{//如同替所有平衡树的寻找方法 
 86             x-=s[s[now].son[0]].sub_size+1;
 87             if(!x) return now;now=s[now].son[1];
 88             //一直找,直到你找到,返回排名 
 89         }
 90     }
 91 }
 92 inline void reverse(int x,int y){//真.反转区间 
 93     int l=x-1,r=y+1;l=find(l),r=find(r);
 94     //要反转区间,你要反转的区间很可能不是恰好属于一个根的两个区间
 95     //所以这时,你要想办法把这个区间变为一个根的两个子树,然后反转子树 
 96     //这时,你可以找到一条可以包含这个区间的最小的链,即[x-1,y+1] 
 97     splay(l,0);splay(r,l);
 98     // 当l成为根,r成为根下面的点时,原本要翻的区间就是一棵子树 
 99     // 由于中序遍历是序列的性质,你可以发现,当你这么干时,他就会成为左右子树 
100     int pos=s[root].son[1]; 
101     pos=s[pos].son[0];s[pos].tag^=1;
102     //标记最初打在操作区间的根节点上
103     //但此时你并没有真的反转,由上面下放标记的函数可以知道 
104 }
105 inline void dfs(int now){
106     pushdown(now);//按照中序遍历输出序列 
107     if(s[now].son[0]) dfs(s[now].son[0]);
108     if(s[now].val!=-INF&&s[now].val!=INF) printf("%d ",s[now].val);
109     if(s[now].son[1]) dfs(s[now].son[1]);
110 }
111 int main(){
112     scanf("%d%d",&n,&m);
113     ori[1]=-INF,ori[n+2]=INF;
114     //-INF&&INF相当于两个标兵,其中ori[1] 就是不存在的,但是可以方便你旋转的超级根 
115     //INF就是防止你在区间反转时 没有r+1 
116     for(register int i=1;i<=n;i++) ori[i+1]=i;//序列的初始化 
117     root=build_tree(1,n+2,0);
118     for(register int i=1;i<=m;i++) scanf("%d%d",&x,&y),reverse(x+1,y+1);
119     dfs(root);
120 } 

然后就是普通平衡树为例

#include<iostream>
#include<cstdio>
using namespace std;
const int MAXL=100005;
const int INF=2147480000;
//你要用到的数字 
struct node{
    int v,father;
    int ch[2];//判断是右孩子(1)还是左孩子(0) 
    int sum;//自己和自己下面还有多少节点 
    int recy;//自己重复出现了几次(元素) 
}e[100001];//左大右 
int n,points;//使用了多少节点,出现了几个元素
//节点每种元素只有一个,但是每种元素都可以有很多个 
int root=e[0].ch[1];//根节点是超级根的右孩子
//全局变量

void update(int x){
    e[x].sum=e[e[x].ch[0]].sum+e[e[x].ch[1]].sum+e[x].recy;
} //维护树的性质 
inline int identify(int x){
    return e[e[x].father].ch[0]==x?0:1; 
}//判断自己是父节点的左孩子还是右孩子
//以上函数在下面的函数中都会用到
 
void connect(int x,int f,int son){
    e[x].father=f;
    e[f].ch[son]=x;
}//作用:将x连接在f的下方。连接方向由son的值决定。
//连接函数。用法:connect(son,father,左儿子(0)或右儿子(1))
void rotate(int x){
    int y=e[x].father;
    int mroot=e[y].father;
    int mrootson=identify(y);
    int yson=identify(x);
    int B=e[x].ch[yson^1];
    connect(B,y,yson);connect(y,x,(yson^1));connect(x,mroot,mrootson);
    update(y);update(x);
}//旋转函数
void splay(int at,int to){
    //伸展函数,splay精髓 
    to=e[to].father;
    //每次都好引用,连接时直接当原本to的父节点(感觉自己在说废话) 
    while(e[at].father!=to){
        //直到at到了原本to的位置才停 
        int up=e[at].father;
        if(e[up].father==to) rotate(at);
        //此时at再向上一下就到了 
        else if(identify(up)==identify(at)){//at,to是同一方向上的子树 
            rotate(up);rotate(at);
        }
        else{//at,to不是同一方向上的子树 
            rotate(at);rotate(at);
        }
    }
}
//connct和rotate隶属于splay 

int crepoint(int v,int father){//新加一个节点 
    n++;e[n].v=v;
    e[n].father=father;
    e[n].sum=e[n].recy=1;
    return n;
}
void destroy(int x){//删除节点 彻底地 
    e[x].v=e[x].ch[0]=e[x].ch[1]=e[x].sum=e[x].father=e[x].recy=0;
    if(x==n) n--;
}
//以上函数隶属于插入元素(注意是元素的插入)与删除元素->建树函数 

//(注意以上两个函数是对节点的操作) 
int find(int v) {
    int now=root;
    while(true){//一直找 
        if(e[now].v==v){
            splay(now,root);
            return now;
        }
        int next=v<e[now].v?0:1;
        if(!e[now].ch[next]) return 0;
        now=e[now].ch[next];
    }
}
//找到特定值及其编号,每次找到后splay以保证树的随机性、

int build(int v){//内部调用的插入函数,没有splay 
    points++;//元素多了一个 
    if(n==0){//特判无点状态 
        root=1;crepoint(v,0);
    }
    else{
        int now=root;
        while(true){//向下找到一个空节点 
            e[now].sum++;//自己的下级肯定增加了一个节点 
            if(v==e[now].v){
                e[now].recy++;//这个值又出现了一次,在原地有俩点了 
                return now;//不用再找了 
            }
            int next=v<e[now].v?0:1;//左孩子,右孩子 
            if(!e[now].ch[next]){
                crepoint(v,now);e[now].ch[next]=n;
                return n;
            }
            now=e[now].ch[next];
        }
    }
    return 0;
} 
//建树函数隶属于插入节点(是节点)和删除节点(是节点) 

void push(int v){//增加节点 
    int add=build(v);splay(add,root);
}
void pop(int v){//删除节点 
    int deal=find(v);//出现过么 
    if(!deal) return;
    points--;//删去了一个元素 
    if(e[deal].recy>1){//这种元素有不止一个 
        e[deal].recy--;e[deal].sum--;
        return;
    }
    if(!e[deal].ch[0]){//要删去的没有左孩子 
        root=e[deal].ch[1];//右孩子当根 
        e[root].father=0;//爸爸没儿子了 
    }
    else{
        //有左孩子,把左孩子中子树的最大当为左孩子的值
        //把原本根的右孩子当为现在左孩子的右孩子,再让左孩子当根 
        int lef=e[deal].ch[0];
        while(e[lef].ch[1]) lef=e[lef].ch[1];
        splay(lef,e[deal].ch[0]);
        int rig=e[deal].ch[1];
        connect(rig,lef,1);connect(lef,0,1);
        update(lef);
    }
    destroy(deal);
}

int rank(int v){//获取值为v的元素在这棵树里是第几小 
    int ans=0,now=root;
    while(true){
        if(e[now].v==v) return ans+e[e[now].ch[0]].sum+1;
        if(now==0) return 0;
        if(v<e[now].v) now=e[now].ch[0];
        else{
            ans=ans+e[e[now].ch[0]].sum+e[now].recy;
            now=e[now].ch[1];
        }
    }
    if(now) splay(now,root);
    return 0;
}
int atrank(int x){//获取第x小的元素的值 
    if(x>points) return -INF;//如果没有,就要返回无限小 
    int now=root;
    while(true){
        int minused=e[now].sum-e[e[now].ch[1]].sum; 
        if(x>e[e[now].ch[0]].sum&&x<=minused) break;
        //如果这个排名大于左子树可又到不了右子树,就直接返回e[now].v 
        if(x<minused) now=e[now].ch[0];//反之,就去左子树找 
        else{
            x=x-minused;//如果都不是 ,就去右子树找 
            now=e[now].ch[1];
        }
    }
    splay(now,root);
    return e[now].v;
}
//无论是第K小的数,还是 k是第几小 都要在找完后splay

int upper(int v){//寻找前驱 
    int now=root;
    int result=INF;
    while(now){
        if(e[now].v>v&&e[now].v<result) result=e[now].v;
        if(v<e[now].v) now=e[now].ch[0];
        else now=e[now].ch[1];
    }
    return result;
}
int lower(int v){//寻找该值对应的一个最近的下界值 
    int now=root;
    int result=-INF;
    while(now){
        if(e[now].v<v&&e[now].v>result) result=e[now].v;
        if(v>e[now].v) now=e[now].ch[1];
            else now=e[now].ch[0];
    }
    return result;
}

 

转载于:https://www.cnblogs.com/fallen-down/p/10809282.html

可持久化splay是一种数据结构,它是对splay树进行修改和查询的一种扩展。在传统的splay树中,对树的修改操作会破坏原有的树结构,而可持久化splay树则允许我们对树进行修改、查询,并且可以保存修改后的每个版本的树结构。 在可持久化splay树中,我们不会直接对原树进行修改,而是通过复制每个节点来创建新的版本。这样,每个版本都可以独立地修改和查询,保留了原有版本的结构和状态。每个节点保存了其左子树和右子树的引用,使得可以在不破坏原有版本的情况下进行修改和查询。 为了实现可持久化splay树,我们可以使用一些技巧,比如引用中提到的哨兵节点和假的父节点和孩子节点。这些技巧可以帮助我们处理根节点的旋转和其他操作。 此外,可持久化splay树还可以与其他数据结构相结合,比如引用中提到的可持久化线段树。这种结合可以帮助我们解决更复杂的问题,比如区间修改和区间查询等。 对于可持久化splay树的学习过程,可以按照以下步骤进行: 1. 理解splay树的基本原理和操作,包括旋转、插入、删除和查找等。 2. 学习如何构建可持久化splay树,包括复制节点、更新版本和保存历史版本等。 3. 掌握可持久化splay树的常见应用场景,比如区间修改和区间查询等。 4. 深入了解与可持久化splay树相关的其他数据结构和算法,比如可持久化线段树等。 在解决问题时,可以使用二分法来确定答案,一般称为二分答案。通过对答案进行二分,然后对每个答案进行检查,以确定最终的结果。这种方法可以应用于很多问题,比如引用中提到的在线询问问题。 综上所述,可持久化splay是一种对splay树进行修改和查询的扩展,可以通过复制节点来创建新的版本,并且可以与其他数据结构相结合解决更复杂的问题。学习过程中可以按照一定的步骤进行,并且可以使用二分法来解决一些特定的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [[学习笔记]FHQ-Treap及其可持久化](https://blog.csdn.net/weixin_34283445/article/details/93207491)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [可持久化数据结构学习笔记](https://blog.csdn.net/weixin_30376083/article/details/99902410)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值