Splay简介
Splay树是一种高效率的BST(二叉搜索树),他的基本操作是把节点旋转到二叉树的根部,旋转操作能有效改善树的平衡性。这个操作附带实现了一个重要应用——高效率访问“热数据”。例如,在计算机网络中,交换机上的路由表的某些IP地址是“热”地址,需要被频繁查询。若用Splay树存储路由表,可以把这些热地址旋转到二叉树的根部,从而尽快被查询到。
Splay树的整体效率也很高,他的平摊操作次数为O(log2 n),也就是说,在一棵有n个结点的BST上做M次Splay操作,时间复杂度为O(Mlog2 n)。
如何设计把一个节点x旋转到根的方法?需要达到一下两个目的:
(1)每次旋转,节点x就上升一层,从而能在有限次操作后到达根部。
(2)旋转能改善BST的平衡性,即尽量使二叉树的层次变少,从根到叶子结点的路径变短,平均的层数和路径长度为O(log2 n)。
Splay树保证时间复杂度的核心就是每操作一个节点,将该节点旋转到树根(局部性原理)。
如果只考虑目的(1),那么使用Treap树的旋转法即可,每次x与它的父节点交换位置,上升一层。称这种旋转为“单旋”,单旋不会减少二叉树的层数,对改善平衡性没有帮助。Splay树主要使用“双旋”,即两次单旋,同时旋转3个结点:x,父亲f,祖父g。双旋分为两种:一字旋,之字旋,能改善平衡性。
左旋右旋不改变树的中序遍历
(1)一字型:先旋转f和g,在旋转x
(2)之字型:先旋转x和f,在旋转x和g。
Splay树插入节点:先按照BST树的规则,找到插入的位置,插入之后,将该插入节点旋转到树根(保证时间复杂度)。
Splay树的删除一段区间操作(本题用的同样的思路,用的懒标记):加入要删除中序遍历的[L,R],那么我们就Splay(L-1,0),将第L-1个结点旋转到树根,然后Splay(R+1,L-1)将R+1 旋转到L-1下面,此时删除R+1节点的左子树就相当于删除了[L,R]区间。
题目描述
您需要写一种数据结构(可参考题目标题),来维护一个有序数列。
其中需要提供以下操作:翻转一个区间,例如原有序序列是 5 4 3 2 1,翻转区间是 [2,4][2,4] 的话,结果是 5 2 3 4 1。
输入格式
第一行两个正整数 𝑛,𝑚,表示序列长度与操作个数。序列中第 𝑖 项初始为 𝑖i。
接下来 𝑚 行,每行两个正整数 𝑙,𝑟,表示翻转的区间。
输出格式
输出一行 𝑛 个正整数,表示原始序列经过 𝑚次变换后的结果。
输入输出样例
输入 #1复制
5 3 1 3 1 3 1 4
输出 #1复制
4 3 2 1 5
说明/提示
【数据范围】
对于 100% 的数据,1≤𝑛,𝑚≤100000,1≤𝑙≤𝑟≤𝑛。
对于本题需要维护的信息:
(1)size:表示子树的大小,用于中序输出下标的指示。
(2)懒标记flag:表示这个区间是否需要反转。
代码如下:
//时刻保证中序遍历是我们当前序列的顺序(只有一开始是有序的,左儿子比当前小,右儿子比当前大)
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m;
struct Node
{
int s[2],p,v;//左右儿子,父节点,当前节点编号
int size;//表示子树大小
int flag;//表示有没有翻转
void init(int _v,int _p)
{
v=_v;
p=_p;
size=1;
}
}tr[N];
int root,idx;
void pushup(int x)//子结点更新父节点
{
tr[x].size=tr[tr[x].s[0]].size+tr[tr[x].s[1]].size+1;
}
void pushdown(int x)//下传懒标记
{
if(tr[x].flag)//如果当前子树需要翻转
{
swap(tr[x].s[0],tr[x].s[1]);//交换左右结点
//将懒标记下传
tr[tr[x].s[0]].flag^=1;
tr[tr[x].s[1]].flag^=1;
//清空当前节点的标记
tr[x].flag=0;
}
}
void rotate(int x)//左旋右旋操作
{
int y=tr[x].p;//y表示x的父节点
int z=tr[y].p;//z表示y的父节点
int k=tr[y].s[1]==x;//k==0表示x是y的左儿子,k==1表示x是y的右儿子
tr[z].s[tr[z].s[1]==y]=x;//如果y是z的左儿子,tr[z].s[1]==y的值为0,否则为1
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;
//先算y的信息,在算x的信息
pushup(y);
pushup(x);
}
void splay(int x,int k)
{
while(tr[x].p!=k)
{
int y=tr[x].p;//y是x的父节点
int z=tr[y].p;//z是y的父节点
if(z!=k)
if((tr[y].s[1]==x)^(tr[z].s[1]==y))//如果是折线关系
rotate(x);
else//直线关系
rotate(y);
//如果z==k的话,转1次就行了
rotate(x);
}
if(!k)//如果k是0,更新根节点
root=x;
}
void insert(int v)
{
int u=root,p=0;//p是u的父节点
while(u)
{
p=u;
u=tr[u].s[v>tr[u].v];//判断v应该插入到u的左儿子还是右儿子
}
u=++idx;//新分配一个编号
if(p)//如果有父节点,那么父节点更新儿子信息
tr[p].s[v>tr[p].v]=u;
tr[u].init(v,p);
//用来保证splay的时间复杂度
splay(u,0);//将这个点旋转到根节点
}
int get_k(int k)//返回中序遍历的第k个数字的下标
{
int u=root;
while(true)
{
pushdown(u);
if(tr[tr[u].s[0]].size>=k) //第k个在左子树
u=tr[u].s[0];
else if(tr[tr[u].s[0]].size+1==k)//第k个数刚好就是根节点
return u;
else
{
k-=tr[tr[u].s[0]].size+1;
u=tr[u].s[1];
}
}
return -1;//没有找到
}
void output(int u)//输出树的中序遍历
{
pushdown(u);//将懒标记下传
if(tr[u].s[0]) //如果左子树不为空,就先遍历左子树
output(tr[u].s[0]);
if(tr[u].v>=1&&tr[u].v<=n)//如果当前节点不是哨兵节点,就直接输出当前节点
printf("%d ",tr[u].v);
if(tr[u].s[1])//如果当前节点的右子树不为空,就递归遍历右子树
output(tr[u].s[1]);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<=n+1;i++) insert(i);//头和尾是哨兵节点,防止越界
while(m--)
{
int l,r;
scanf("%d%d",&l,&r);
l=get_k(l),r=get_k(r+2);//要找到L的前驱节点与R的后继节点,(本来是要找L-1,和R+1),因为0号点是哨兵,所以多加1
splay(l,0);//将左端点旋转到根节点
splay(r,l);//将右端点转到左端点的下面
tr[tr[r].s[0]].flag^=1;//将右端点的左子树翻转
}
output(root);//输出树的中序遍历
return 0;
}