非旋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/