treap,顾名思义tree+heap既有平衡树的性质:左儿子权值<本节点权值<右儿子权值,中序遍历有序,且树高尽可能小又有heap的性质:键值满足小根堆,左儿子的键值和右儿子的键值都大于本节点的键值而FHQ Treap仅仅通过两个函数就能够进行维护,思想好懂,代码也很简洁。更重要的是相比于之前的替罪羊树或者pbds这种只能维护大小关系的平衡树而言,它可以胜任splay拥有的维护区间的特质。首先是普通的维护大小关系:split函数,将一棵树按照给定的权值v划分成两棵树,并且保证第一棵树的权值均小于等于v,第二棵树的权值均大于v。显然只要递归分裂的树,考虑当前节点的权值和给定的v的关系即可。注意分裂的两个节点传引用。merge函数,将两棵树合成一棵树,要求第一棵树的权值均小于等于第二棵树同样是递归考虑,那么当前两棵树谁的根作为合并的树的根呢?按照权值来看都是可以的,但是为了保持平衡性,就要维护键值的小根堆性质,所以让键值小的那个节点作为根即可。注意合并的那个节点要传引用。由于split和merge都会改变父子关系,所以最后要pushup维护好子树的信息。而对于普通平衡树所支持的插入、删除、第k大、排名、前驱、后继等等,只要基于split和merge两个函数就可以轻松实现,比较简单, 懒得写了p6136:fhq treap 8.2s,替罪羊5.81s,不过fhq treap代码短点。当然两者都很好理解。一般情况下fhqtreap足以应对所有情况(除了LCT以及重度卡常题(紫荆花之恋?听说),所以splay还是得懂)
1 |
|
然后是维护区间,也很简单,改一下split和merge的定义即可。split,给定一个树,把一棵树的前k个节点划分给一棵树,剩余节点划分给另一棵树merge,将两棵树合并成一棵树,需要保证一棵树完全处在另一棵树的前面写法和上面类似,不再赘述考虑对区间[l,r]的操作怎么做:先把rt的前r个节点split给x,其他的丢给y再把x的前l-1个节点split给z此时x就是[l,r]区间内所有节点构成的treap的根了,直接在根上打标记即可。最后按照顺序z,x,y进行merge。考虑pushup和pushdown在什么时候进行。pushup是要维护当前子树的信息,因此需要在左右儿子改变后调用,而pushdown是为了下放赖在当前节点上的懒标记,所以要在左右儿子改变前调用,想清楚这两点就知道了,应该在split和merge进入子树前pushdown,在回溯上来之后再pushup。p3391 文艺平衡树,区间翻转,显然只要把要操作的区间split出来然后在根上打一个翻转标记即可。
1 |
|
维护区间信息的时候,一定要注意平衡树和线段树是不一样的,线段树只要左右儿子取优即可(因为本节点维护的区间是左右儿子维护的区间的并),而平衡树左右儿子维护的信息是不包含本节点的,所以还需要维护本节点的值,和该值再取优。在维护区间的时候,就丧失了权值大小的比较了,那么如何知道编号为i的节点在区间中的哪个位置呢?显然从根开始是不知道该走左还是右的。但是如果我们从i节点不断跳父亲直至根,然后不断维护小于你的节点个数就可以了,而treap的树高是$\log n$级别的,复杂度没问题。要维护节点的父亲,一种简单的做法是在pushup的时候维护一下p的左右儿子的父亲为p,但这样子是有问题的,根节点的fa值是错误的,没法及时清零,当然如果只有一棵treap,且知道根节点的编号,就可以通过==rt来判断,否则就应该在split和merge中维护fa:
1 | void split(int p, int &x, int &y, int fx, int fy, int k) |
返回编号为p的节点所在treap的根节点,以及在原数组中的下标:
1 | pair<int, int> get(int p) |