[总结] 无旋treap
- 导语 -
顾名思义就是没有旋转操作的treap.
还是很好打的.
毕竟旋转操作旋转上天.
- 学习 -
两个核心操作: split和merge
split是将一棵树分成两棵树的操作.
注意这里的要求是对于确定的树,将其前k个点分成新树, 剩下的点变成另一颗新树,因此可能出现多个切割的地方.
对于一个节点来说,我们必然只会处理它的一颗子树,因此用递归去找处理的子树就行了.
返回时对于每一个点更新一下它被处理的那颗子树.
函数的返回值是两颗子树的根.
具体看代码吧
define pii pair<int,int>
define mp make_pair
pii split(int rt, int k) { //对于根为rt的树,将它前k个点裂成一棵树A,剩下的点成为树B
if (!rt) return mp(0, 0);
pii tmp;
pushdown(rt);
if (k > S[C[rt][0]]) { //处理右子树
tmp = split(C[rt][1], k - S[C[rt][0]] - 1);
//tmp表示右子树分裂出来的两棵树(A,B),
//其中左边(A)的是当前rt的新的右子树
C[rt][1] = tmp.first; pushup(rt); tmp.first = rt;
//更新rt, 然后将rt作为一个新的左子树,
//原右子树分裂出来的右边的新树(B)作为新的右子树,
//将这颗新树返回
}
else { //处理左子树
tmp = split(C[rt][0], k);
C[rt][0] = tmp.second; pushup(rt); tmp.second = rt;
}
return tmp;
}
merge即是将两颗树合并的操作, 注意这里合并的树(A,B)要求max_value(A) < min_value(B),这样把AB一左一右相接(即保证B的每一个节点都在A的右边)便保证了权值的有序,我们就只要维护堆的性质了.(显然split分出来的两颗树就满足这样的性质)
int merge(int ra, int rb) { //返回新树的根
if (!ra) return rb;
if (!rb) return ra; //有一颗空树,直接合并
pushdown(ra);
pushdown(rb);
if (KEY[ra] < KEY[rb]) { //ra的key值较小,维护小根堆的话要放在上面
C[ra][1] = merge(C[ra][1], rb);
//默认rb是接在右边的树,因此rb必然会接进ra的右子树中
pushup(ra); return ra;
//不要忘记更新
}
else {
C[rb][0] = merge(ra, C[rb][0]);
pushup(rb); return rb;
}
}
- 单点操作 -
这里是题目.
单点操作基本都能靠merge+split完成.
比如这题只需加上splay中一样的getkth(找到第k个数), findkth(找到数A的位置),
那么:
insert=getkth(findkth+getkth)+split+merge
delete=getkth(findkth+getkth)+split+merge
单点插入删除也可用(merge)(split)完成.
- 区间操作 -
这里是题目.
其实和单点操作没什么区别...
区间的插入删除也是使用(merge)(split)完成.
删除好说,但是注意插入时需要我们先建好一颗子树再merge.
于是又有了一个build函数.
我们可以一个一个把点插到新树中去(一开始有一颗空树).
那么每次插入的点必然在树的最右端.
然后开始维护小根堆的性质.
考虑root -> right son -> right son ... 这样一条链, 我们先把新点接在这条链最下面,
然后找到其中深度最小的一个key值大于大于点的节点,把以它为根的子树当做新点
的左子树, 然后用新点代替它原来的位置就可以了.(相当于把新点沿着链一直向上旋)
但是需要注意排布在这条链上的树是没有维护(pushup/update)过的, 因此每次
寻找到要被移到新点下面的点都需要一次pushup, 最后再给仍在链上的点来一发pushup.
因为每次加入的点都在链上, 可以证明每个点都会(在它的所有子树之后)经过一次pushup
还有一个需要注意的点是splay中的虚点.
无旋treap并不需要虚点,但是在pushup的时候可能考虑到空子树的情况,为避免空子树的影响
需要一个初始化.