非旋转Treap:用时间换时间的有效手段
Hello大家好,我们今天来聊一聊非旋转Treap。
相信各位或多或少都做过些序列上的问题。如果水题我们考虑暴力;不强制在线我们可能用过莫队和待修改莫队;不更改序列上的时间戳信息的我们使用线段树或者树状数组,也有可能请出主席树。那如果更大幅度的操作,我们要用到splay或者块链。但是!我们今天介绍一种特殊的平衡树,用时间换取时间的数据结构。
啥意思?如果写过splay和块链,前者的调试时间和后者的敲版子时间让我们头痛欲裂,但是这个平衡树,好写好调,省去了时间。但是常数相对较大,这便是我们的代价(好一个时间换时
间)。
Treap就是Tree+heap,一个基于堆的二叉搜索树,但是它的维护过程是仰仗于查找的。但是我们今天讲的非旋转Treap,顾名思义,不需要旋转的哦。
那应该如何维护平衡呢?用两个基本操作即可:撕裂&合并。
撕裂??考虑区间最本质的:如果我们可以将要修改的区间成为一棵单独的树,我们是不是想干什么就干什么!整体操作就打标记,特殊操作就特殊处理。我们也想要这样怎么办?引入一个split操作,就是断开原来BST上的一条边,整个BST变成了两棵树,返回一个pair。如果想要提取一个区间,就split两次,中间的子树就是我们要的区间子树。 附上版子(为了方便理解,我每个语句都是一行):
// split函数的pair存的是两个新BST的两个根,时间戳小的在first,时间戳大的在second pair split(int pos,int num) //表示在以pos为根节点的BST中,取出前num个作为新的BST,剩下的作为另一棵BST { if(num==0)//递归退出条件,如果在一个pos的BST取出前0个,我们就返回一个(0,pos)的pair。 { return make_pair(0,pos); } int lson=a[pos].ls; int rson=a[pos].rs; // 这里我们分类讨论 if(num==a[lson].size) // 如果恰好是左子树的大小,我们将左子树断开。 { a[pos].ls=0;//将左子树和根节点之间的路径断开 pushup(pos);//更新当前根节点信息。 return make_pair(lson,pos);//返回两个新子树的pair,这里返回的是lson而不是a[pos].ls是因为后者已经被更新成0. } // 后面的同理 else if(num==a[lson].size+1) { a[pos].rs=0; pushup(pos); return make_pair(pos,rson); } else if(num<a[lson].size) { pait t=split(lson,num); a[pos].ls=t.second; pushup(pos); return make_pair(t.first,pos); } else { // 这里我们需要注意:如果num>a[lson].size+1,说明我们分开的两棵子树中有一棵包含了pos的左子树和根节点。 pair t=split(rson,num-a[lson].size-1); // 所以这里我们直接将右子树分成num-a[lson].size-1的两个子树。 a[pos].rs=t.first; // 然后将num-a[lson].size-1大小的子树和前面的树加在一起,这样就得到了大小为num的子树。 pushup(pos); return make_pair(pos,t.second); } }
合并??如果我们将一棵单独的表示区间的BST处理完了,我们需要将两棵子树合并到一起啊!所以就有了这个merge操作。因为我们本质上维护平衡的办法就是利用随机数的权值来维护一个依据权值的堆,撕裂操作显然不会打破这个局势,但是如果我们瞎合并,就无法保证平衡,时间复杂度也就没有办法保证。所以我们想一个能维护两个子树的方法,即能保证堆的性质,用保留BST应该有的中序遍历时间戳递增。我们请出可并堆!显然,如果我们采用可并堆的合并方式,是可以达到我们预期的效果的。
可并堆是如何合并的呢?其实也非常简单,这样: