更快的带交集无旋 Treap 合并

维护可重集的合并

一般手法

  • 权值线段树: 均摊时间 \(O(n\log n)\),还可以支持分裂,但空间开销巨大
  • 平衡树启发式合并:空间 \(O(n)\) 但总时间高达两个 \(\log\)

非旋 Treap 合并

这个科技的时间复杂度为均摊 \(O(n\log n)\),但我不会证(带分裂应该是假的)。在这里感谢 Mr_Spade 给我介绍这个(并不算非常复杂的)科技。

考虑现在有两棵 Treap,根为 \(x, y\)。我们先比较两个结点的随机值,钦定随机值小的作为当前的根。这里假定为 \(x\)。然后我们需要搞出 \(x\) 的左右子树,分别为两棵 Treap 并集(除去 \(x\))中 \(< x\)\(>x\) 的权值的结点构成的 Treap(\(=x\) 的可以特殊处理)。

对于 \(x\),显然它的左右子树(\(l_1, r_1\))就满足上面那个要求;而对于 \(y\) 我们则可以直接按 \(x\) 的权值 split,得到 \(l_2, r_2\) 两棵树。

最后我们发现这是一个可以递归处理的问题,因为我们再对 \(l_1, l_2\)\(r_1,r_2\) 分别做这样的合并即可,两次合并的结果就可以作为 \(x\) 的两个子树。

参考代码实现:

int join(int x, int y) {
    if (!x || !y) return x | y; // 有一个空间的即可返回
    if (t[x].pty > t[y].pty) swap(x, y); // 取随机权值小的作为根
    int L1 = t[x].ch[0], R1 = t[x].ch[1], L2 = 0, R2 = 0, equ = 0; // x 直接是左右子树
    split(y, t[x].val, L2, R2), split(L2, t[x].val - 1, L2, equ); // y 按权值 split
    if (equ) t[x].cnt += t[equ].siz, t[x].siz += t[equ].siz; // 相等特殊处理
    t[x].ch[0] = join(L1, L2), t[x].ch[1] = join(R1, R2); // 递归合并
    return pushup(x), x; // 更新信息
}

实际上这样常数并不大,在空间优于线段树的同时也不会比线段树慢。

效率测试

题目:Luogu P5494 【模板】线段树分裂。由于此题带分裂所以复杂度有点问题,但可以测试效率。

先放一个 评测结果(I/O 优化,O2)。这个排在时间最优解第一页(2020-10-23,现在好像被一堆卡常巨怪挤出去了QAQ),去掉 I/O 优化的话空间用的也很少。

posted @ 2020-10-23 19:44  -Wallace-  阅读(2049)  评论(1编辑  收藏  举报