splay区间翻转

splay区间翻转
「BZOJ3223」JoyOI 1729 文艺平衡树
Sample Input
5 3
1 3
1 3
1 4
Sample Output
4 3 2 1 5
#include
#include
using namespace std;//splay区间翻转,翻转的是结点的下标,结点的值不动的
struct node{
    int sum,flag,siz;//结点值,翻转标记,子树结点数
    node *ch[2],*fa;//儿子指针,父亲指针
    void pushdown(node *nd){//传入的结点就是null,哇,好无聊啊,明明是全局变量了
        if(flag){//对传入的结点判是否被打了标记
            swap(ch[0],ch[1]);//有标记就交换
            if(ch[0]!=nd) ch[0]->flag^=1;//左儿子非空,翻转标记反转
            if(ch[1]!=nd) ch[1]->flag^=1;//右儿子非空,翻转标记反转
            flag=0;//本结点标记清0
        }
    }
    void update(){siz=ch[0]->siz+ch[1]->siz+1;}//当前结点子树结点数是两儿子和+1
}pool[4000010],*tail=pool,*root,*null;//内存池,尾针,根针,空针(这都要建?)
int n,m;
void init(){
    null=++tail;//空针指向尾针+1即POOL数组下标为1元素
    null->siz=0;//子树结点数为0
    null->ch[0]=null->ch[1]=null;//左右儿子都是空
    null->sum=null->flag=0;//结点值与翻转标记都是0
}
node* newnode(node *fa){//传入结点创新点
    node *nd=++tail;//尾针加一指向数组
    nd->fa=fa;//当前结点的老豆就是传进来的那个结点
    nd->siz=1;//子树结点1
    nd->ch[1]=nd->ch[0]=null;//左右儿子都是空
    nd->flag=0;//翻转标记为0
    return nd;//返回新建结点的指针
}
node *build(node *fa,int lf,int rg){//0~N+1结点建树,传入当前结点及其控制的左右界
    if(lf>rg) return null;//左界大于右界表示空
    node *nd=newnode(fa);//由传进来的老豆结点创建当前新结点
    if(lf==rg){//如点是叶子结点
        nd->sum=lf;//其值就是下标的值
        return nd;//返回当前结点指针
    }
    int mid=(lf+rg)>>1;
    nd->sum=mid;//关键:非叶子结点的值是其控制区间的中间值(实即整棵树每个结点都是)
    nd->ch[0]=build(nd,lf,mid-1);//递归创建左儿子且返回指针赋给左儿边
    nd->ch[1]=build(nd,mid+1,rg);//递归创建右儿子且返回指针赋给右儿边
    nd->update();//本结点子树结点数更新
    return nd;//第一层即最终返回就是根结点,根结点的FA就是NULL
}
void rot ( node*& x, int d ){
    node* y=x->fa;//读出当前结点老豆,下面就是旋转模板啦
    y->ch[!d]=x->ch[d];
    if(x->ch[d]!= null) x->ch[d]->fa=y ;
    x->fa=y->fa ;
    if(y->fa!=null)
        (y==y->fa->ch[0]) ? y->fa->ch[0]=x : y->fa->ch[1]=x;
    x->ch[d]=y;//右旋时原老豆Y就成现结点X的右儿,左旋同理
    y->fa =x;//X成了Y的老豆
    x->update();//更新子树结点数
    y->update();//更新子树结点数
}
void Splay(node *nd,node *tar){//把ND结点移到TAR
    while(nd->fa!=tar){//当前结点老爸未为target目标就继续旋转,注意传左界时是NULL,即其老豆是NULL时跳出,此时他就是根了,而传右界时target目标是根,即当其老豆是根时就跳出,刚好符合!
        node *ne=nd->fa;//读出当前结点的老豆
        if(nd==ne->ch[0]){//是老豆左儿
            if(ne->fa!=tar&&ne==ne->fa->ch[0])//老爸不是target目标结点且爷爸孙三者一字形
               rot(ne,1);//老豆右旋
            rot(nd,1);//当前结点右旋
        }
        else{//是老豆右儿
            if(ne->fa!=tar&&ne==ne->fa->ch[1])//老爸不是target目标结点且爷爸孙三者一字形
               rot(ne,0);//老豆左旋
            rot(nd,0);//当前结点左旋
        }
    }
    if(tar==null) root=nd;//如果传进的目标结点是空,那么就更新一下根结点
}
node *kth(node *nd,int K){//返回第K小的结点,树堆规定:左儿值<当前点值<右子值
    nd->pushdown(null);//有翻转先压下
    if(nd->ch[0]->siz+1==K) return nd;//左儿子树结点+1是K,注意有0结点,此时ND指的就是K-1的那个结点
    if(nd->ch[0]->siz+1>K) return kth(nd->ch[0],K);//如果大过K则肯定在左儿中
    else return kth(nd->ch[1],K-nd->ch[0]->siz-1);//否则肯定在右儿中
}
void rev(int L,int R){//翻转L,R区间,精华开始
    node *x=kth(root,L);//得到下标为L-1的结点指针
    node *y=kth(root,R+2);//得到下标为R+1的结点指针
    Splay(x,null);//X转到根,请注意SPLAY函数的结束条件
    Splay(y,root);//Y转到X的右儿子,请注意SPLAY函数的结束条件,理解关键一步为何是X右儿
    y->ch[0]->flag^=1;//根的右儿的左儿加翻转标记(就是这一步完成了翻转)
}
void GrandOrder(node *nd){
    if(nd==null) return ;//结点空跳出
    nd->pushdown(null);//把翻转标记压一下
    GrandOrder(nd->ch[0]);//递归出左儿子
    if(nd->sum>=1&&nd->sum<=n)printf("%d ",nd->sum);//当前结点值是1~N就输出(因为有两个0与N+1的结点)
    GrandOrder(nd->ch[1]);//递归出右儿子
}
int main(){
    init();
    int L,R;
    scanf("%d%d",&n,&m);
    root=build(null,0,n+1);//0~N+1建点
    while(m--){//M次翻转操作
        scanf("%d%d",&L,&R);//输入L,R
        rev(L,R);//翻转LR
    }
    GrandOrder(root);
    return 0;
}
解释:
题面还是比较良心的 
看上去只需要一种操作,就是区间翻转,但是其他数据结构好像不是很好维护这个操作 
首先谈谈SPLAY是一个什么东西 
SPLAY首先是一颗BST,它有着BST共有的性质,即满足左小右大的性质,而旋转是BST几乎共有的一种操作,旨在不破坏BST性质的情况下调整树的结构 
中序遍历是一种树的遍历方式,只要树是BST,那中序遍历的节点顺序就不会改变。因为中序遍历是左中右的顺序,而BST满足左小右大的性质,对于任意一个点,比他小的点在不修改的前提下是固定的,所以中序遍历的结果也不会改变 
SPLAY的核心操作就是SPLAY,它的意思是将某一个节点通过旋转的方式,不破坏BST,到达指定位置。但指定位置一定得在它上面。这么做的目的是使得被多次访问的点尽可能的靠近根节点,这样在后续访问过程中使得访问尽量短,虽然每次访问会多了将访问节点旋转到根的时间,但却使后续对相同节点的访问时间缩短,从而使时间复杂度“平摊”,确保总操作复杂度趋于稳定。 
而实现的方法其实也相对简单,就是旋转 
如果目标节点就是当前节点的父亲,那直接旋转便是 
否则采用双旋的办法,往上跳。 
双旋右分2种 
一种是当前节点的父亲,和当前节点父亲的父亲,也就是爷爷,三者形成了链式结构,就是说父亲是爷爷的左儿子,儿子也是父亲的做儿子,或者父亲是爷爷的右儿子,儿子也是父亲的右儿子,这样的,我们就先对父亲进行旋转,再旋转儿子 
另一种是之字型结构,就是父亲是爷爷的左儿子,儿子是父亲的右儿子,或者父亲是爷爷的右儿子,儿子是父亲的左儿子,这样,就连续两下把儿子转上去 
至于为什么不一样我也不是很懂,ZJC大神说链式结构连续单旋会被卡~~~~ 
再有就是其独特的区间翻转的性质。首先要实现区间翻转的话,就要明确是对什么建的一颗BST,并不是区间里的值,而是区间的下标。在转来转去的过程中,需要保证中序遍历一定是按区间下标12345。。。排好了的。 
区间翻转饿第一步是找到需要翻转的区间,比如[L,R],这里的L也好,R也好,都是指区间的下标,而由于是对区间下标建的BST,所以要查下标对应的节点的位置是很容易的。而这里我们需要找到的是L-1和R+1的位置,通过SPLAY的方式将L-1节点转到根节点,R+1节点转到根节点的右子树,因为L-R下标一定比L-1大,这又是一颗BST,所以L-R区间一定在L-1,就是根节点的右边,同理一定在R+1,即根节点的右子树的左边,所以L-R对应的位置只能是且一定在根节点右儿子的左子树。 
当然SPLAY只能把节点转到其祖先的位置,而如果L-1和R+1都在根节点的右子树或左子树怎么办哪 
其实先SPLAY(L-1) 时,就已经使R+1在根节点的右边了。。。 
这时L-1就是新的根了,而L,R就在根节点右子树的左子树。这时我们就使从根节点右儿子的左儿子的两个儿子开始,交换左右儿子,在逐层交换左右儿子 
想想为什么这样是对的 
假设总区间是1-10,要反转的区间是3-8,模拟一下SPLAY过程,则现在根节点是2,其右儿子是9,而右儿子的左儿子是5,而其子树就是3-8的区间了,5的左边是1-4,右边是6-8,交换左右儿子后就使得1-4到了右边,而6-8到了左边,是符合翻转定义的。 
而考虑下一层,3的右边是4,而翻转后应该4在3的左边,交换左右子树。 
考虑这样递归下去,类似于归并排序的思路,每个区间逐层翻转然后合并,而翻转前挨在一起的数翻转后必然还是挨在一起,所以是可以区间分治然后合并的 
当然我们并不需要每次翻转就直接翻到底,不然成本太高,而是打一个标记,类似于线段树,在查询的时候在随着查询的深入而随便翻转 

 

你可能感兴趣的:(ACM笔记-2串树)