Splay(伸展树、分裂树):平衡二叉搜索树中功能最丰富的树

这是我第一篇对高级数据结构的描述,如有不准确的地方还请指出,谢谢~

调这颗树的代码调的头皮发麻,和线段树根本不是一个难度的。

首先简单地介绍一下这棵平衡BST中的另类

这是一棵没有任何平衡因子的BST,它依靠均摊来达到O(logn)的插入查询和删除复杂度,常数比较大

而且,它的具有其他BST所不具备的,对于子树的任意分裂和合并的功能

下面我从定义讲起,剖析这棵树实现过程中的每一个细节

const int INF=1000000000;
const int maxn=1000005;
int n,m;
int a[maxn];
struct Tree
{
    int fa,ch[2];
    int size;
    int v,sum,mx,lx,rx;
    bool tag,rev;
}t[maxn];
int node[maxn];
int root;
int cnt=0;
queue<int> q;

定义部分,注意INF如果太大可能会有越界的危险,所以这里长了个教训

树中包含n个点,对于树中的每一个点,有如下定义:

fa里存父节点的ID,ch[]里存左右孩子的ID,size里存该节点所形成子树包含的总节点个数

v表示节点的值,sum表示该节点所形成子树的值的和

mx,lx,rx是需要维护的一个关系,其中:

mx是当前子树的最大子串和

lx是当前子树以左端点为起点,向右延伸的最大子串和,rx同理(这里的实现思路类似于SCOI2010的线段树的那道题)

tag是区间重置标记,rev是区间翻转标记,区间即子树

q是一个垃圾回收的缓冲数组,将在后面介绍

建树部分:

void buildtree(int l,int r,int f)
{
    if(l>r)
        return;
    int mid=(l+r)>>1;
    int o=node[mid];
    int last=node[f];
    if(l==r)
    {
        t[o].sum=a[l];
        t[o].size=1;
        t[o].tag=t[o].rev=0;
        if(a[l]>=0)
            t[o].lx=t[o].rx=t[o].mx=a[l];
        else
        {
            t[o].lx=t[o].rx=0;
            t[o].mx=a[l];
        }
    }
    else
    {
        buildtree(l,mid-1,mid);
        buildtree(mid+1,r,mid);
    }
    t[o].v=a[mid];
    t[o].fa=last;
    update(o);
    t[last].ch[mid>=f]=o;
}

依然是递归建树,由于是均摊的,根节点只要从中间随便找一个就好了

这里可以类比线段树的建树过程,比线段树稍微复杂一些

建树过程中用到了一个函数update:

void update(int o)
{
    int l=t[o].ch[0],r=t[o].ch[1];
    t[o].sum=t[l].sum+t[r].sum+t[o].v;
    t[o].size=t[l].size+t[r].size+1;
    t[o].mx=max(t[l].mx,t[r].mx);
    t[o].mx=max(t[o].mx,t[l].rx+t[o].v+t[r].lx);
    t[o].lx=max(t[l].lx,t[l].sum+t[o].v+t[r].lx);
    t[o].rx=max(t[r].rx,t[r].sum+t[o].v+t[l].rx);
}

它的作用是随着子树的建立随时去更新子树根节点所维护的那些值

接下来介绍插入操作,它可以在特定位置插入tot个元素:

void insert(int k,int tot)
{
    for(int i=1;i<=tot;i++)
        cin>>a[i];
    for(int i=1;i<=tot;i++)
    if(!q.empty())
    {
        node[i]=q.front();
        q.pop();
    }
    else
    {
        node[i]=++cnt;
    }

    buildtree(1,tot,0);
    int z=node[(1+tot)>>1];
    int x=find(root,k+1);
    int y=find(root,k+2);
    splay(x,root);
    splay(y,t[x].ch[1]);
    t[z].fa=y;
    t[y].ch[0]=z;
    update(y);
    update(x);
}

大致过程如下。用着tot个元素生成一棵新的子树,子树上随便找一个点作为根节点

之后对原树伸展操作,首先找到k+1与k+2位置的节点ID

然后将k+1位置的节点伸展到根节点位置,将k+2位置的节点伸展到新根节点(就是刚才伸展过来的)的右子树位置

然后直接把新生产的子树的根节点接在k+2位置节点的左子树位置上就好了,形成之字形结构,有三层,LRL,分别是XYZ

有了这种插入的思路,我们可以把任意的东西接在任意的位置,这就是合并操作的原理

在介绍删除操作之前,我们先介绍一下split操作:

int split(int k,int tot)
{
    int x=find(root,k),y=find(root,k+tot+1);
    splay(x,root);
    splay(y,t[x].ch[1]);
    return t[y].ch[0];
}

其作用是把k位置之后的tot个节点形成的子树拎出来,放在之前所描述的之字形结构的Z位置,然后就能随意处理这个子树了,你可以把它删掉,也可以再把它接到其他位置

就比如刚才在描述插入操作的时候的那个Z位置,这样就实现了把区间的一段挪到区间的另一个位置去。分裂合并操作也就隐式实现了

既然现在这个split出来的子树已经和原来的树没有任何关系了,我就开一个新变量存一下它的根节点,然后把它和原树的联系切断,为了介绍切断,我们这里引出删除函数:

void erase(int k,int tot)
{
    int x=split(k,tot);
    int y=t[x].fa;
    rec(x);
    t[y].ch[0]=0;
    update(y);
    update(t[y].fa);
}

其删除的实现中有一个回收节点空间的函数rec,我们如果此时不做回收,而是像刚才那样,开一个新变量存这个拎出来的节点,就能既切断联系又拎出来了一棵子树。美滋滋

接下来我们稍微改动一下insert函数,把其中z替换成我们拎出来的这个子树

综上所述,区间的任意分裂合并都可以实现了

其实除了分裂和合并操作之外,就是BST的最基本的套路了

我们接着介绍,刚才引出了好几个陌生的函数,先说离咱们最近的那个,rec,回收子树节点函数:

void rec(int x)
{
    if(!x)
        return;
    int l=t[x].ch[0],r=t[x].ch[1];
    rec(l);rec(r);q.push(x);
    t[x].fa=t[x].ch[0]=t[x].ch[1]=0;
    t[x].tag=t[x].rev=0;
}

这个函数存在的意义就是重复利用Tree结构的空间,避免因数组连续性造成的空间浪费,这里学了一招,应该很实用

然后我们介绍find函数,这个函数就是用来找指定位置的节点ID的

int find(int o,int rk)
{
    pushdown(o);
    int l=t[o].ch[0],r=t[o].ch[1];
    if(t[l].size+1==rk)
        return o;
    if(t[l].size>=rk)
        return find(l,rk);
    return find(r,rk-t[l].size-1);
}

在o形成的子树中找第k个节点的ID

这里又引出了pushdown函数

void pushdown(int x)
{
    int l=t[x].ch[0];
    int r=t[x].ch[1];
    if(t[x].tag)
    {
        t[x].rev=t[x].tag=0;
        if(l)
        {
            t[l].tag=1;
            t[l].v=t[x].v;
            t[l].sum=t[x].v*t[l].size;
        }
        if(r)
        {
            t[r].tag=1;
            t[r].v=t[x].v;
            t[r].sum=t[x].v*t[r].size;
        }
        if(t[x].v>=0)
        {
            if(l)
                t[l].lx=t[l].rx=t[l].mx=t[l].sum;
            if(r)
                t[r].lx=t[r].rx=t[r].mx=t[r].sum;
        }
        else
        {
            if(l)
                t[l].lx=t[l].rx=0,t[l].mx=t[x].v;
            if(r)
                t[r].lx=t[r].rx=0,t[r].mx=t[x].v;
        }
    }
    if(t[x].rev)
    {
        t[x].rev^=1;t[l].rev^=1;t[r].rev^=1;
        swap(t[l].lx,t[l].rx),swap(t[r].lx,t[r].rx);
        swap(t[l].ch[0],t[l].ch[1]);
        swap(t[r].ch[0],t[r].ch[1]);
    }
}

它是用来处理节点上的翻转和重置标记的

说了半天都没有说splay函数,下面介绍

void splay(int x,int &k)
{
    while(x!=k)
    {
        int y=t[x].fa;
        int z=t[y].fa;
        if(y!=k)
        {
            if(t[y].ch[0]==x^t[z].ch[0]==y)
                rotate(x,k);
            else
                rotate(y,k);
        }
        rotate(x,k);
    }
}

这个函数可以把一个节点伸展到指定根节点的位置(可以是子树根),这个时候这个节点就是新的根节点了

这是整个伸展树最核心的函数,一定要理解

然后就是旋转操作,其实刚开始旋转我是不会的,后来我是去看了AVL的四种旋转方式才过来看的Splay

我们先回顾一下AVL的四种旋转方式,什么是AVL(一种特别特别正经的平衡树,不想用)

LL,RR,LR,RL,左旋就是往左转,右旋就是往右转,转的时候要搞清楚转哪条边,把谁转过去把谁转过来,转完之后有时候要拆接,怎么处理。搞清楚之后,理解Splay的一字形旋转和之字形旋转就容易多了

Splay双旋(只有双旋才叫Splay)的目的不是为了平衡,而是为了协助完成Splay操作,一次Splay就把沿途的所有的点都转了一遍

下面给出旋转的函数,写的比较硬核:

void rotate(int x,int &k)
{
    int y=t[x].fa;
    int z=t[y].fa;
    int l=(t[y].ch[1]==x);
    int r=l^1;
    if(y==k)
        k=x;
    else
        t[z].ch[t[z].ch[1]==y]=x;
    t[t[x].ch[r]].fa=y;
    t[y].fa=x;
    t[x].fa=z;
    t[y].ch[l]=t[x].ch[r];
    t[x].ch[r]=y;
    update(y);
    update(x);
}

后面的就比较水了,重点都已经介绍完了,再说说修改函数,把tot个树都修改为指定的值

void modify(int k,int tot,int val)
{
    int x=split(k,tot);
    int y=t[x].fa;
    t[x].v=val,t[x].tag=1,t[x].sum=t[x].size*val;
    if(val>=0)
        t[x].lx=t[x].rx=t[x].mx=t[x].sum;
    else
        t[x].lx=t[x].rx=0,t[x].mx=val;
    update(y);
    update(t[y].fa);
}

原理很简单,打标记,更新节点参数

然后是区间翻转:

void rever(int k,int tot)
{
    int x=split(k,tot);
    int y=t[x].fa;
    if(!t[x].tag)
    {
        t[x].rev^=1;
        swap(t[x].ch[0],t[x].ch[1]);
        swap(t[x].lx,t[x].rx);
        update(y);
        update(t[y].fa);
    }
}

也是打标记,反正有处理标记的函数,不怕,这里和线段树的lazy Tag的原理类似,只不过这里的树实在是很复杂

再说说查询:

void query(int k,int tot)
{
    int x=split(k,tot);
    cout<<t[x].sum<<endl;
}

只查个sum太水了,我们可以中序遍历是不是,BST家族都可以的

特别特别特别要注意的一点,在执行分裂合并操作的时候,如果你没有保证分裂合并能够满足BST性质,那么因为你的分裂合并,这棵Splay可能就不再是一棵BST了

你要特别小心这一点,仔细审题,如果只让你分裂合并,那好说,如果涉及到了查询什么的,一定要看清楚题意才行

最后,我们给出debug了三个小时的Template,感谢黄学长~

  1 #include<queue>
  2 #include<iostream>
  3 #include<cstdlib>
  4 #include<algorithm>
  5 using namespace std;
  6 const int INF=1000000000;
  7 const int maxn=1000005;
  8 int n,m;
  9 int a[maxn];
 10 struct Tree
 11 {
 12     int fa,ch[2];
 13     int size;
 14     int v,sum,mx,lx,rx;
 15     bool tag,rev;
 16 }t[maxn];
 17 int node[maxn];
 18 int root;
 19 int cnt=0;
 20 queue<int> q;
 21 void update(int o)
 22 {
 23     int l=t[o].ch[0],r=t[o].ch[1];
 24     t[o].sum=t[l].sum+t[r].sum+t[o].v;
 25     t[o].size=t[l].size+t[r].size+1;
 26     t[o].mx=max(t[l].mx,t[r].mx);
 27     t[o].mx=max(t[o].mx,t[l].rx+t[o].v+t[r].lx);
 28     t[o].lx=max(t[l].lx,t[l].sum+t[o].v+t[r].lx);
 29     t[o].rx=max(t[r].rx,t[r].sum+t[o].v+t[l].rx);
 30 }
 31 void pushdown(int x)
 32 {
 33     int l=t[x].ch[0];
 34     int r=t[x].ch[1];
 35     if(t[x].tag)
 36     {
 37         t[x].rev=t[x].tag=0;
 38         if(l)
 39         {
 40             t[l].tag=1;
 41             t[l].v=t[x].v;
 42             t[l].sum=t[x].v*t[l].size;
 43         }
 44         if(r)
 45         {
 46             t[r].tag=1;
 47             t[r].v=t[x].v;
 48             t[r].sum=t[x].v*t[r].size;
 49         }
 50         if(t[x].v>=0)
 51         {
 52             if(l)
 53                 t[l].lx=t[l].rx=t[l].mx=t[l].sum;
 54             if(r)
 55                 t[r].lx=t[r].rx=t[r].mx=t[r].sum;
 56         }
 57         else
 58         {
 59             if(l)
 60                 t[l].lx=t[l].rx=0,t[l].mx=t[x].v;
 61             if(r)
 62                 t[r].lx=t[r].rx=0,t[r].mx=t[x].v;
 63         }
 64     }
 65     if(t[x].rev)
 66     {
 67         t[x].rev^=1;t[l].rev^=1;t[r].rev^=1;
 68         swap(t[l].lx,t[l].rx),swap(t[r].lx,t[r].rx);
 69         swap(t[l].ch[0],t[l].ch[1]);
 70         swap(t[r].ch[0],t[r].ch[1]);
 71     }
 72 }
 73 void rotate(int x,int &k)
 74 {
 75     int y=t[x].fa;
 76     int z=t[y].fa;
 77     int l=(t[y].ch[1]==x);
 78     int r=l^1;
 79     if(y==k)
 80         k=x;
 81     else
 82         t[z].ch[t[z].ch[1]==y]=x;
 83     t[t[x].ch[r]].fa=y;
 84     t[y].fa=x;
 85     t[x].fa=z;
 86     t[y].ch[l]=t[x].ch[r];
 87     t[x].ch[r]=y;
 88     update(y);
 89     update(x);
 90 }
 91 void splay(int x,int &k)
 92 {
 93     while(x!=k)
 94     {
 95         int y=t[x].fa;
 96         int z=t[y].fa;
 97         if(y!=k)
 98         {
 99             if(t[y].ch[0]==x^t[z].ch[0]==y)
100                 rotate(x,k);
101             else
102                 rotate(y,k);
103         }
104         rotate(x,k);
105     }
106 }
107 int find(int o,int rk)
108 {
109     pushdown(o);
110     int l=t[o].ch[0],r=t[o].ch[1];
111     if(t[l].size+1==rk)
112         return o;
113     if(t[l].size>=rk)
114         return find(l,rk);
115     return find(r,rk-t[l].size-1);
116 }
117 void rec(int x)
118 {
119     if(!x)
120         return;
121     int l=t[x].ch[0],r=t[x].ch[1];
122     rec(l);rec(r);q.push(x);
123     t[x].fa=t[x].ch[0]=t[x].ch[1]=0;
124     t[x].tag=t[x].rev=0;
125 }
126 int split(int k,int tot)
127 {
128     int x=find(root,k),y=find(root,k+tot+1);
129     splay(x,root);
130     splay(y,t[x].ch[1]);
131     return t[y].ch[0];
132 }
133 void query(int k,int tot)
134 {
135     int x=split(k,tot);
136     cout<<t[x].sum<<endl;
137 }
138 void modify(int k,int tot,int val)
139 {
140     int x=split(k,tot);
141     int y=t[x].fa;
142     t[x].v=val,t[x].tag=1,t[x].sum=t[x].size*val;
143     if(val>=0)
144         t[x].lx=t[x].rx=t[x].mx=t[x].sum;
145     else
146         t[x].lx=t[x].rx=0,t[x].mx=val;
147     update(y);
148     update(t[y].fa);
149 }
150 void rever(int k,int tot)
151 {
152     int x=split(k,tot);
153     int y=t[x].fa;
154     if(!t[x].tag)
155     {
156         t[x].rev^=1;
157         swap(t[x].ch[0],t[x].ch[1]);
158         swap(t[x].lx,t[x].rx);
159         update(y);
160         update(t[y].fa);
161     }
162 }
163 void erase(int k,int tot)
164 {
165     int x=split(k,tot);
166     int y=t[x].fa;
167     rec(x);
168     t[y].ch[0]=0;
169     update(y);
170     update(t[y].fa);
171 }
172 void buildtree(int l,int r,int f)
173 {
174     if(l>r)
175         return;
176     int mid=(l+r)>>1;
177     int o=node[mid];
178     int last=node[f];
179     if(l==r)
180     {
181         t[o].sum=a[l];
182         t[o].size=1;
183         t[o].tag=t[o].rev=0;
184         if(a[l]>=0)
185             t[o].lx=t[o].rx=t[o].mx=a[l];
186         else
187         {
188             t[o].lx=t[o].rx=0;
189             t[o].mx=a[l];
190         }
191     }
192     else
193     {
194         buildtree(l,mid-1,mid);
195         buildtree(mid+1,r,mid);
196     }
197     t[o].v=a[mid];
198     t[o].fa=last;
199     update(o);
200     t[last].ch[mid>=f]=o;
201 }
202 void insert(int k,int tot)
203 {
204     for(int i=1;i<=tot;i++)
205         cin>>a[i];
206     for(int i=1;i<=tot;i++)
207     if(!q.empty())
208     {
209         node[i]=q.front();
210         q.pop();
211     }
212     else
213     {
214         node[i]=++cnt;
215     }
216
217     buildtree(1,tot,0);
218     int z=node[(1+tot)>>1];
219     int x=find(root,k+1);
220     int y=find(root,k+2);
221     splay(x,root);
222     splay(y,t[x].ch[1]);
223     t[z].fa=y;
224     t[y].ch[0]=z;
225     update(y);
226     update(x);
227 }
228 int main()
229 {
230     std::ios::sync_with_stdio(false);
231     cin>>n>>m;
232     t[0].mx=a[1]=a[n+2]=-INF;
233     for(int i=1;i<=n;i++)
234         cin>>a[i+1];
235     for(int i=1;i<=n+2;i++)
236         node[i]=i;
237     buildtree(1,n+2,0);
238     root=(n+3)>>1;
239     cnt=n+2;
240     int k,tot,val;
241     char ch[15];
242     while(m--)
243     {
244         cin>>ch;
245         if(ch[0]!=‘M‘||ch[2]!=‘X‘)
246             cin>>k>>tot;
247         if(ch[0]==‘I‘)
248             insert(k,tot);
249         if(ch[0]==‘D‘)
250             erase(k,tot);
251         if(ch[0]==‘M‘)
252         {
253             if(ch[2]==‘X‘)
254                 cout<<t[root].mx<<endl;
255             else
256                 cin>>val,modify(k,tot,val);
257         }
258         if(ch[0]==‘R‘)
259             rever(k,tot);
260         if(ch[0]==‘G‘)
261             query(k,tot);
262     }
263     return 0;
264 }

最后吐槽一下,这道题是NOI2005年的数列操作,也是我有生以来做的第二道NOI题目(这时候立刻想到了第一道是什么)

能够在考场上完美实现一个这么个东西,平时要付出多少努力可想而知,不是谁都可以去清华的

原文地址:https://www.cnblogs.com/aininot260/p/9326793.html

时间: 2024-10-29 16:24:06

Splay(伸展树、分裂树):平衡二叉搜索树中功能最丰富的树的相关文章

算法dfs——二叉搜索树中最接近的值 II

901. 二叉搜索树中最接近的值 II 中文 English 给定一棵非空二叉搜索树以及一个target值,找到 BST 中最接近给定值的 k 个数. 样例 样例 1: 输入: {1} 0.000000 1 输出: [1] 解释: 二叉树 {1},表示如下的树结构: 1 样例 2: 输入: {3,1,4,#,2} 0.275000 2 输出: [1,2] 解释: 二叉树 {3,1,4,#,2},表示如下的树结构: 3 / 1 4 2 挑战 假设是一棵平衡二叉搜索树,你可以用时间复杂度低于O(n)

二叉搜索树中的常用方法

1 package Tree; 2 3 import org.junit.Test; 4 5 class TreeNode { 6 7 int val = 0; 8 TreeNode left = null; 9 TreeNode right = null; 10 11 public TreeNode(int val) { 12 this.val = val; 13 14 } 15 16 } 17 18 public class BinarySearchTree { 19 20 /** 21 *

leetcode 二叉搜索树中第K小的元素 python

二叉搜索树中第K小的元素 给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素. 说明:你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数. 示例 1: 输入: root = [3,1,4,null,2], k = 1 3 / 1 4   2 输出: 1 示例 2: 输入: root = [5,3,6,2,4,null,null,1], k = 3 5 / 3 6 / 2 4 / 1 输出: 3 进阶:如果二叉搜索树经常被修改(插入/删除操作)并且

Leetcode 701. 二叉搜索树中的插入操作

题目链接 https://leetcode.com/problems/insert-into-a-binary-search-tree/description/ 题目描述 给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树. 返回插入后二叉搜索树的根节点. 保证原始二叉搜索树中不存在新值. 注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可. 你可以返回任意有效的结果. 例如, 给定二叉搜索树: 4 / 2 7 / 1 3 和 插入的值: 5 你可以返回这个

[Swift]LeetCode450. 删除二叉搜索树中的节点 | Delete Node in a BST

Given a root node reference of a BST and a key, delete the node with the given key in the BST. Return the root node reference (possibly updated) of the BST. Basically, the deletion can be divided into two stages: Search for a node to remove. If the n

[LeetCode] 285. Inorder Successor in BST 二叉搜索树中的中序后继节点

Given a binary search tree and a node in it, find the in-order successor of that node in the BST. Note: If the given node has no in-order successor in the tree, return null. 给一个二叉搜索树和它的一个节点,找出它的中序后继节点,如果没有返回null. 解法1: 用中序遍历二叉搜索树,当找到root.val = p.val的时

230. 二叉搜索树中第K小的元素

230. 二叉搜索树中第K小的元素 题意 给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素. 你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数. 解题思路 中序遍历,利用Python3中提供的生成器方法: 中序遍历,判断存储结点值的数组是否到到k,则表明访问的一个结点就是第k个最小的元素: 先获取跟结点处于的位置(第几个最小的元素),如果它比k小,则从右子结点中找,如果它比k大,则从左子节点中找: 实现 class Solution:    

【剑指Offer-树】二叉搜索树中的众数

题目描述 给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素). 假定 BST 有如下定义: 结点左子树中所含结点的值小于等于当前结点的值 结点右子树中所含结点的值大于等于当前结点的值 左子树和右子树都是二叉搜索树 示例: 例如: 给定 BST [1,null,2,2], 1 2 / 2 返回[2]. 题目链接: https://leetcode-cn.com/problems/find-mode-in-binary-search-tree/ 思路 二叉搜索树

230 Kth Smallest Element in a BST 二叉搜索树中第K小的元素

给定一个二叉搜索树,编写一个函数kthSmallest来查找其中第k个最小的元素. 注意:你可以假设k总是有效的,1≤ k ≤二叉搜索树元素个数. 进阶:如果经常修改二叉搜索树(插入/删除操作)并且你需要频繁地找到第k小值呢? 你将如何优化kthSmallest函数? 详见:https://leetcode.com/problems/kth-smallest-element-in-a-bst/description/ 方法一:递归实现 /** * Definition for a binary