zoukankan      html  css  js  c++  java
  • 非旋Treap及其可持久化

    平衡树这种东西,我只会splay。splay比较好理解,并且好打,操作方便。
    我以前学过SBT,但并不是很理解,所以就忘了怎么打了。
    许多用平衡树的问题其实可以用线段树来解决,我们真正打平衡树的时候一般都是维护序列之类的。
    维护序列时,splay特别方便,所以一般情况下打splay就好了。其它的平衡树也可以,可是如果见到翻转操作的时候,那些平衡树就会崩(至少我不知道那些平衡树有什么可以翻转的做法)。
    但是splay的常数很大(常数最大的平衡树),并且有的时候需要可持久化。
    这个时候就要用到非旋Treap来代替splay了。
    但有的时候不能代替,比如LCT,据说只有splay才能让它达到lglg级别,用非旋Treap会变成lg2lg^2。(splaysplay常数大得要死,所以或许也没关系吧)
    话说splay和LCT的博客我都没打,懒得打了


    Treap?

    Treap=Tree+Heap
    Treap上每个节点有两个值(key,val)(key,val)keykey满足BST的性质,valval满足堆的性质
    valval的取值是个随机数,所以说Treap的复杂度是期望的。
    Treap有带旋Treap和非旋Treap,带旋Treap似乎没什么卵用,所以直接说非旋Treap。


    非旋Treap

    (下面的Treap以普通平衡树为例子,和维护序列的Treap不一样)

    核心操作

    sqlit(x,p)sqlit(x,p):将以xx为根的树的前pp个与后面的分开,形成两棵树,返回两棵树的根节点(有序)。
    merge(a,b)merge(a,b):将以aa和以bb为根的子树合并,返回合并后的根节点。(保证aa子树中所有)

    sqlit(x,p)sqlit(x,p)

    判断第pp个点在xx的左边还是右边,然后递归下去。
    回溯的时候在左边或者右边接上。
    具体见代码:

    inline pair<Node*,Node*> sqlit(Node *t,int p){
    	pair<Node*,Node*> res(null,null);
    	if (t==null)
    		return res;
    	if (p<=t->l->siz){
    		res=sqlit(t->l,p);
    		t->l=res.second;
    		t->update();
    		res.second=t;
    	}
    	else{
    		res=sqlit(t->r,p-t->l->siz-1);
    		t->r=res.first;
    		t->update();
    		res.first=t;
    	}
    	return res;
    }
    

    由于每次tt必定往下递归,所以时间是lglg级别的。

    merge(a,b)merge(a,b)

    首先判断如果一个为空,就返回另一个。
    接下来比较aabbvalval的大小,选取小的那个作为根,然后将儿子与另一个进行合并。
    具体见代码:

    inline Node *merge(Node *a,Node *b){
    	if (a==null)
    		return b;
    	if (b==null)
    		return a;
    	if (a->val<b->val){
    		a->r=merge(a->r,b);
    		a->update();
    		return a;
    	}
    	b->l=merge(a,b->l);
    	b->update();
    	return b;
    }
    

    由于每次aabb有一个要向下走,所以时间也是lglg级别的。

    其它操作

    其它操作就比较简单了。
    rank(t,key)rank(t,key):找出keykey的位置(小于它的数的个数+1+1)。
    insert(t,key)insert(t,key):插入keykey
    通过rankrank找出它要插入的位置。
    把前面和后面分开,将其插在中间,三个合并起来。
    remove(t,key)remove(t,key):删除keykey。。
    通过rankrank找出它的位置。
    把前面、它、后面分开,然后前面和后面合并。
    kth(t,k)kth(t,k):找第kk
    pred(t,key)pred(t,key):找keykey的前驱
    succ(t,key)succ(t,key):找keykey的后继
    容易发现这些都是在lglg级别的时间内完成的。

    buildbuild

    这个操作或许有点特殊,可以用笛卡尔树实现,时间是线性的(当keykey有序的时候)。
    笛卡尔树是什么自己上网查去
    考虑到我们有其它操作,所以时间还是O(nlgn)O(nlg n)级别的,所以这样建树费码量还没有多大意义,还不如一个一个插入,时间O(nlgn)O(nlg n)
    但有的时候常数折磨人,所以还是说一下吧。
    建立笛卡尔树的时候,我们维护一个栈,表示最右边的那一条链。
    栈底到栈顶的valval值递增。
    将新的节点插在栈顶的右儿子处,但这可能会破坏堆的性质,所以不行。
    不断弹栈,直到栈顶小于这个点valval。将点插在它的右儿子处,它原来的右儿子变成这个点的左儿子。

    区间操作

    首先,在维护区间的时候,所谓的keykey实际上并不存在,它具体指它们在平衡树中的位置(排名)。
    区间操作的大体思想和splay是差不多的,就是截取一段区间,将它们集中在一棵子树里面,然后进行各种操作。
    我们可以通过sqlitsqlit将这个区间分离出来,然后打上一个标记,再放回去。
    当然标记会下传。
    这样的时间复杂度显然是lglg级别的。
    用这种思想来搞区间操作就可以做到翻转操作。如果用别的平衡树,可以在每个节点上维护它子树的信息,在计算的时候像线段树那样合并。可是这样搞不了翻转啊!

    代码

    题目是洛谷上的普通平衡树。

    using namespace std;
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <cstdlib>
    #include <ctime>
    #define N 100010
    #define irand (rand()*32768+rand())
    int n;
    struct Node{
    	int key,val;
    	Node *l,*r;
    	int siz;
    	inline void update(){
    		siz=l->siz+r->siz+1;
    	}
    } d[N];
    Node *null=d;
    inline pair<Node*,Node*> sqlit(Node *t,int p){
    	pair<Node*,Node*> res(null,null);
    	if (t==null)
    		return res;
    	if (p<=t->l->siz){
    		res=sqlit(t->l,p);
    		t->l=res.second;
    		t->update();
    		res.second=t;
    	}
    	else{
    		res=sqlit(t->r,p-t->l->siz-1);
    		t->r=res.first;
    		t->update();
    		res.first=t;
    	}
    	return res;
    }
    inline Node *merge(Node *a,Node *b){
    	if (a==null)
    		return b;
    	if (b==null)
    		return a;
    	if (a->val<b->val){
    		a->r=merge(a->r,b);
    		a->update();
    		return a;
    	}
    	b->l=merge(a,b->l);
    	b->update();
    	return b;
    }
    int rank(Node *t,int key){
    	if (t==null)
    		return 1;
    	if (key<=t->key)
    		return rank(t->l,key);
    	return t->l->siz+1+rank(t->r,key);
    }
    Node *kth(Node *t,int k){
    	if (k<=t->l->siz)
    		return kth(t->l,k);
    	if (k>t->l->siz+1)
    		return kth(t->r,k-t->l->siz-1);
    	return t;
    }
    Node *pred(Node *t,Node *res,int key){
    	if (t==null)
    		return res;
    	if (t->key<key)
    		return pred(t->r,t,key);
    	return pred(t->l,res,key);
    }
    Node *succ(Node *t,Node *res,int key){
    	if (t==null)
    		return res;
    	if (t->key>key)
    		return succ(t->l,t,key);
    	return succ(t->r,res,key);
    }
    int cnt;
    Node *root;
    int main(){
    	srand(time(0));
    	null->val=2147483647;
    	root=null;
    	scanf("%d",&n);
    	for (int i=1;i<=n;++i){
    		int op,x;
    		scanf("%d%d",&op,&x);
    		if (op==1){
    			int k=rank(root,x);
    			pair<Node*,Node*> a=sqlit(root,k-1);
    			d[++cnt]={x,irand,null,null,1};
    			root=merge(merge(a.first,&d[cnt]),a.second);
    		}
    		else if (op==2){
    			int k=rank(root,x);
    			pair<Node*,Node*> a=sqlit(root,k-1),b=sqlit(a.second,1);
    			root=merge(a.first,b.second);
    		}
    		else if (op==3)
    			printf("%d
    ",rank(root,x));
    		else if (op==4)
    			printf("%d
    ",kth(root,x)->key);
    		else if (op==5)
    			printf("%d
    ",pred(root,null,x)->key);
    		else
    			printf("%d
    ",succ(root,null,x)->key);
    	}
    	return 0;
    }
    

    很短是不是?


    可持久化

    可以参考一下上面的标程,我们发现那些节点不需要记录父亲。
    这就是可持久化的特征啊!就像可持久化线段树一样,每个点都不需要记录父亲,在修改的时候把修改变成新建,然后新建之后照样把儿子指针连过去。
    这就是非旋Treap最伟大的地方:可持久化!
    在非旋Treap上面改一改,把所有的修改操作改为新建操作,就可以了。
    然后你就会发现所需要的空间特别大……
    以下是代码,为洛谷上的可持久化文艺平衡树:

    using namespace std;
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <cstdlib>
    #include <ctime>
    #define N 200010
    #define irand (rand()*32768+rand())
    int n;
    struct Node{
        int val,v;
        Node *c[2];
        int siz;
        long long sum;
        bool rev;
        inline void update(){
            sum=c[0]->sum+c[1]->sum+v;
            siz=c[0]->siz+c[1]->siz+1;
        }
        inline void pushdown();
    } d[N*100];
    Node *null;
    int cnt;
    #define clone(t) (&(d[++cnt]=*(t)))
    inline void Node::pushdown(){
        if (rev){
            if (c[0]!=null)
                c[0]=clone(c[0]);
            if (c[1]!=null)
                c[1]=clone(c[1]);
            swap(c[0],c[1]);
            c[0]->rev^=1;
            c[1]->rev^=1;
            rev=0;
        }
    }
    pair<Node*,Node*> sqlit(Node *t,int k){
        pair<Node*,Node*> res(null,null);
        if (t==null)
            return res;
        t->pushdown();
        if (k<=t->c[0]->siz){
            res=sqlit(t->c[0],k);
            Node *newt=clone(t);
            newt->c[0]=res.second;
            newt->update();
            res.second=newt;
        }
        else{
            res=sqlit(t->c[1],k-t->c[0]->siz-1);
            Node *newt=clone(t);
            newt->c[1]=res.first;
            newt->update();
            res.first=newt;
        }
        return res;
    }
    Node *merge(Node *a,Node *b){
        if (a==null)
            return b;
        if (b==null)
            return a;
        if (a->val<b->val){
        	a->pushdown();
            Node *newa=clone(a);
            newa->c[1]=merge(a->c[1],b);
            newa->update();
            return newa;
        }
        b->pushdown();
        Node *newb=clone(b);
        newb->c[0]=merge(a,b->c[0]);
        newb->update();
        return newb;
    }
    Node *root[N+1];
    int main(){
        srand(time(0));
        null=d;
        null->val=2147483647;
        root[0]=null;
        scanf("%d",&n);
        long long lastans=0;
        for (int i=1;i<=n;++i){
            int pre,op;
            scanf("%d%d",&pre,&op);
            if (op==1){
                int p,x;
                scanf("%d%d",&p,&x),p^=lastans,x^=lastans;
                Node *t=&(d[++cnt]={irand,x,null,null,1,x,0});
                pair<Node*,Node*> a=sqlit(root[pre],p);
                root[i]=merge(merge(a.first,t),a.second);
            }
            else if (op==2){
                int p;
                scanf("%d",&p),p^=lastans;
                pair<Node*,Node*> a=sqlit(root[pre],p-1),b=sqlit(a.second,1);
                root[i]=merge(a.first,b.second);
            }
            else if (op==3){
                int l,r;
                scanf("%d%d",&l,&r),l^=lastans,r^=lastans;
                pair<Node*,Node*> a=sqlit(root[pre],l-1),b=sqlit(a.second,r-l+1);
                b.first->rev^=1;
                root[i]=merge(a.first,merge(b.first,b.second));
            }
            else{
                int l,r;
                scanf("%d%d",&l,&r),l^=lastans,r^=lastans;
                pair<Node*,Node*> a=sqlit(root[pre],l-1),b=sqlit(a.second,r-l+1);
                printf("%lld
    ",lastans=b.first->sum);
                root[i]=root[pre];//很容易发现合并回去和之前是一模一样的,所以直接用之前那个。
            }
        }
        return 0;
    }
    

    说实在的,这个程序的常数特别大。
    在洛谷上跑得贼慢,而且还有点听天由命的味道……因为Treap是随机的,运气不好就会相差个几千毫秒。
    无用的空间特别多,比如,插入操作中,从原来的树中拆出两棵树,然后合并。在上面的程序中,这两棵拆出的树也会被保存,然而我们只需要保存合并之后的树就好了。如何解决?其实可以在拆出来的时候用新建的方法,然后在合并的时候就不用新建了,像普通的非旋Treap一样。这样子可以大大地节省空间,应该也能节省时间。
    只不过,不想打了啊!都已经快3000多byte了……
    然后就是一个值得深思的问题,可不可以做到不下传翻转标记?上面的程序中,下传标记就要暴力地新开节点,消耗很多空间。我们能不能用标记永久化之类的思想来搞一下?我一开始就是那样打的,可不知道为什么错了,也许是我自己的方法有漏洞。
    还有,询问的时候,我考虑过使用类似于线段树的操作,每个节点都可以表示成一个区间,询问区间就是几个节点的答案合并起来,显然节点的个数是lglg级别的。然后我打了,本来以为会快,结果……更慢了。

    我觉得我很有必要重新学习一下卡常数。

  • 相关阅读:
    大三学习进度64
    大三学习进度70
    中美科技巨头——BATH和GAFA
    多线程写excel数据思路
    3
    1
    比特币
    加分项
    3e
    换题了
  • 原文地址:https://www.cnblogs.com/jz-597/p/11145237.html
Copyright © 2011-2022 走看看