不需要旋转,却能力压群雄的数据结构——非旋Treap 看完还不会你打我

非旋Treap讲解

Treap,一种平衡树。作为一棵平衡树,一定是遵从着某种原则,使得这棵树尽量的接近完全二叉树。除了二叉搜索树都具备的性质—— 左子树 ≤ 根 ≤ 右子树,顾名思义,Treap = tree+heap。这时他的 特殊性质就飘出水面了——heap

有一个需要慢慢理解的东西,我觉得是本文最重要的——二叉搜索树中顺次排列所参考的参数是灵活的,谁说非要按数值??有的题你就得用下标。
一、每个node的元素组成
对于一个node,有fix(随机分配的数,用rand即可),size(这个node所引领的树的大小,包括自己),剩下的参数视情况而定。这棵树所包含点的fix值,需要满足堆的性质,一般情况下我们会使用最小堆(约定俗成)。以bzoj3224为例:
struct Treap {
    Treap *l, *r;
    int fix, key, size; //fix表示随机值,key表示数值,size表示以它为根的这棵子树的大小
    Treap(int key_):fix(randad()), key(key_), l(NULL), r(NULL), size(1) {}
    inline void updata() {
        //更新树的大小
        size = 1 + (l?l->size:0) + (r?r->size:0);
    }
}*root;
typedef pair<Treap*, Treap*> Droot;
inline int Size(Treap *x) {return x?x->size:0;}
 
 
 
一定要注意!在改了某节点后,一定要update!
二、基础的操作
有人会说,既然都不旋转了,那肯定操作起来特别难。好吧我告诉你们,真的挺难。不过只要认真看完我的文章(老脸一红)并多多练习,我相信你一定能使用的得心应手。
①Split
先下个定义:Split(x, k)表示,把x引导的这一棵树的前k个节点(顺序是按照左子树 ≤ 根 ≤ 右子树来划定的)从树中分离出来。函数的返回结果是一个节点对,记作(x1, x2),此时x1所引导的树就是前k个节点,x2引导的树就是剩下的节点。 注意:Split之后,我们已经真真切切的把它们切开了,现在我们的Treap已经不是一个整体了,一定要养成好习惯,Split之后不要忘了再合并起来(合并是我们要讲的下一个基础操作)。
那具体怎么做呢?
Droot Split(Treap *x, int k) {
    if(!x) return Droot(NULL, NULL);
    Droot y;
    if(Size(x->l) >= k) { //如果左子树的节点数能达到k个,则前k个一定都在左子树中
        y = Split(x->l, k);
        x->l = y.second; //x的左子树只留下来了不在前k个位置的节点
        x->updata();
        y.second = x; //此时x这棵树可以整个拖上去,作为以前不在前k个位置的节点集合
    }else {
        y = Split(x->r, k - Size(x->l) - 1); //要在右子树搜索前几个,连同根节点与左子树,作为前k个
        x->r = y.first; //x的右子树变为以前的右子树的前几个(此时x的size应恰为k)
        x->updata();
        y.first = x; //整个拖上去,作为前k个位置的点的集合
    }
    return y;
}
②Merge
继续下定义:Merge(x,y)表示,把x这棵树与y这棵树合并起来(必须满足的条件:在划分顺序时,x中所有元素都应排在y中的元素的前面。所以我认为Merge最重要的性质在于 有序性)。函数的返回结果是一个节点,表示合并后这棵树的树根。
Treap *Merge(Treap *A, Treap *B) {
    if(!A) return B;
    if(!B) return A;
    if(A->fix < B->fix) { //如果A的随机数比较小,那他应该居上
        A->r = Merge(A->r, B); //而B本就应该排A后面,于是把A的右子树和B合并,一并作为A的右子树(注意顺序)
        A->updata();
        return A;
    }else { //B居上
        B->l = Merge(A, B->l); //把A和B的左子树合并,一并作为B的左子树
        B->updata();
        return B;
    }
}

于是,到现在,基本的操作你就都会了。

我们来应用一下。
看一下bzoj3224,要我们支持的操作有6个,我们一一来分析。
①先说查询一个数的排名。就和普通二叉搜索树没什么区别的,直接上代码。
inline int Getkth(Treap *x, int v) {
    //询问一个数是第几小 
    if(!x) return 0;
    int ans = 0, temp = (int)2e9+1;
    while(x) {
        if(v == x->key) temp = min(temp, ans + Size(x->l) + 1);
        if(v > x->key) ans+= Size(x->l) + 1, x = x->r;
        else x = x->l;
    }
    return temp==(int)2e9+1?ans:temp;
}
②顺便一块把查询第几名是谁说了吧。异曲同工。
int Findkth(int k) {
    //寻找第k小
    Treap *p; p = root;
    while(true) {
        if(Size(p->l) == k - 1) return p->key;
        if(Size(p->l) > k - 1) p = p->l;
        else k-= (Size(p->l) + 1), p = p->r;
    }
}
③Insert:只加入一个数,于是我们先查询,在平衡树中有几个比x小的(用Getkth,这就是为什么我先讲查排名),记为k,然后把树分开,把节点放中间,合并。
inline void Insert(int v) {
    int k = Getkth(root, v);
    Droot x = Split(root, k);
    Treap *n = new Treap(v);
    root = Merge(Merge(x.first, n), x.second);
    return ;
}
④Delete:删除。与insert一样,分成三段——前k-1个,第k个,后面剩下的。直接合并第1、3段即可。(如果必要,最好随删随清内存,这就是指针的好处。但指针的坏处就是极其不可控,容易re)
删内存代码:
void del(Treap *p) {
	if(!p) return ;
	if(p->l) del(p->l);
	if(p->r) del(p->r);
	delete p;
}
⑤⑥前驱与后继,这个很简单,就是利用查排名函数。
inline int pre(Treap *k, int x) {
    int ans = (int)-2e9-1;
    while(k) {
        if(k->key < x) ans = max(ans, k->key), k = k->r;
        else k = k->l;
    }
    return ans;
}

inline neg(Treap *k, int x) {
    int ans = (int)2e9+1;
    while(k) {
        if(k->key > x) ans = min(ans, k->key), k = k->l;
        else k = k->r;
    }
    return ans;
}

再看一个应用,笛卡尔树。我们知道,加点的复杂度是log级的,批量加点复杂度显然有一点大(其实我只是想去掉一点常数,log级并没有那么大,但是oj也许会卡)。
批量加数时,先存进数组a中,a[0]表示一共有多少数要进树。
Treap *Build(int *a) {
	static Treap *x, *last;
	int p = 0;
	for(int i = 1; i <= a[0]; ++i) {
		x = new Treap(a[i]);
		last = NULL;
		while(p && sta[p]->fix > x->fix) {
			sta[p]->updata();
			last = sta[p];
			sta[p--] = NULL;
		}
		if(p) sta[p]->r = x;
		x->l = last;
		sta[++p] = x;
	}
	while(p) sta[p--]->updata();
	return sta[1];
}

至此,Treap的基本知识已经讲完了。如果大家有兴趣看一下平衡树的操作boss题,我推荐bzoj1500维修数列。
附上我的维修数列Treap版代码:http://paste.ubuntu.com/26194333/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值