P6136
【模板】普通平衡树(数据加强版)
给一个自己的建议,学习平衡树真的不要太多,我个人觉得学会 FHQ-Treap(可以处理大部分需要平衡树的问题)和 Splay(主要是 LCT 必须要用)就够了。。。
学太多了我觉得大部分人应该很难背下来。。。我自己光是背这两个板子就花了相当的时间。。。
如果非要论优先级的话,建议先学 FHQ-Treap,它灵活性又好、代码又短还易于理解。。。
说实话普通 Treap 似乎不用学,因为这个东西除了常数小之外,功能相比于 FHQ-Treap 和 Splay 都要弱“一些”,更重要的是:代码真的也不短(可很多 OI 的书上都会写这个,而且还不写 FHQ-Treap)。。。
很多数据结构都是比较工具性的东西,真的。。。
虽然这两个平衡树据说常数很巨大,但我实在学不动了咕咕咕。
一、Splay
原来的题拿了一个 Treap 过了。
然后没有任何封装,导致模板很难再利用什么的,比如说在树套树上。
然后这里写了一个封装的 Splay。
应该比较好用。
但是没有卡常。所以直接加了 inline。好像也能快上不少。
可能 Splay 天生常数大跑不过 Treap 吧。
时间复杂度 $O((n+m)\log n)$。
$Update AD20220123$ 代码修改。使用指针写法为 Splay 动态分配内存,更加方便应用在树套树等嵌套数据结构上。
实际上比起单纯去理解,背掉代码反而能理解得更快。
Init
。这个操作为一棵 Splay 分配大小为 $x$ 的内存空间。Get
。这个操作用来返回 $x$ 是左儿子还是右儿子。Pushup
。这个操作用来更新 $x$ 自身的子树大小(在不同的题可以有不同的用途,和线段树的Pushup
类似)。Clear
。这个操作用来清空一个节点。Rotate
。这个操作是Splay
的基础。然而总结起来就是:如果 $x$ 是 $y$ 的一边的儿子,则 $x$ 的另一边的儿子变成 $y$ 一边的儿子,$x$ 的另一边的儿子变成 $y$。
splay
。这个操作是 Splay 的核心操作,一共有六种情况。然而背代码比背情况方便得多。而且代码背完之后就能很好的理解这六种情况了。Find
。这个操作是一个辅助操作,用来找到数值为 $k$ 的节点。具体操作就是和节点比大小决定往左还是往右。最后把找到的点
splay
到根节点。理论上来讲,如果说 Splay 中尚未插入这个节点,则这个操作返回的节点是前驱或后继。Ins
。这个操作是插入一个节点。首先若树是空的直接插入即可。否则,我们左右寻找即可,可以证明它一定可以找到一个空节点的位置插入。
Rk
。不是 Mark 而是 Rk 因为没有马。原来我单门又写了一个函数实现,但实际上这个操作可以借助Find
直接实现。判断一下它在根节点的左边还是右边即可(左边就直接输出左子树大小加一,右边就输出左子树大小加根节点大小加一)。
Kth
。先判断与左子树的大小关系,如果大于等于的话减掉左子树和根节点,此时若小于等于零,则我们找到了该节点,先
splay
该节点再返回该节点,否则往右子树寻找;否则往左子树寻找。Pre
与Nxt
。找前驱与后继。这里先
Find
,然后如果根节点此时就是前驱或者后继直接返回即可。反之,以前驱为例,先走到左子树,再一直往右走,这个点就是前驱。后继同理。当然图省事的话直接
Rk
和Kth
结合起来也是完全没有问题的(不过常数大一点)。Del
。这个操作会比想象中的复杂一些,因为多了一些特殊情况的讨论。先
Find
,然后如果说这个点的大小大于一那么直接减大小即可。反之判断特殊情况,如果左右子树都没有,直接把树删完;若没有右子树或左子树,直接删根节点;反之找到根的前驱,前驱成为此时的根,则要删的点必然没有左子树,因此直接把它的右子树接给前驱,然后删除即可。
至此,Splay 的所有操作结束。
什么,如果有延迟标记怎么搞下传?请看文艺平衡树。
现在这个代码是我修的比较漂亮的状态了,但仍然有点长。(我见过最短的大概是 lxl 的 WBLT,大概不到百行,当然压行很厉害)
代码:
1 |
|
二、FHQ-Treap
我来填坑了/kk。
我还是以这种类似代码注释的方式搞吧。
Init
。和 Splay 类似,为 FHQ-Treap 动态分配大小为 $x$ 的内存。Pushup
。更新 $x$ 自身子树大小。Clear
。清空一个节点。Split
。这个是 FHQ-Treap 的重要操作之一。这里的 $x$ 和 $y$ 是两个虚拟节点,帮助我们索引分裂后的子树应该放到哪里(并且方便的分裂出确实的两个树)。
我们分裂是根据权值与关键值 $k$ 的比较来的,简单来说,假如我们比较到节点 $p$,如果 $val(p)\le k$,显然 $p$ 及其左子树的所有节点都是权值小于等于 $k$ 的;反之就是 $p$ 及其右子树都是权值大于 $k$ 的。然后我们再递归处理另一个子树的分裂。
Merge
。这个也是 FHQ-Treap 的重要操作之一。合并先从两颗 Treap 的根节点开始。很容易知道两个 Treap 实际上是有序的,并且我们默认左边的 Treap 是权值更小的。
所以我们可以直接根据二者的随机键值来判断父子关系(需要满足堆性质)。然后讨论是 $x$ 和 $y$ 的左子树合并还是 $y$ 和 $x$ 的右子树合并即可。
最后需要注意空节点的时候直接返回即可。
Ins
。这个表示向 Treap 中插入一个数。这个我们先把 Treap 分裂成两部分,这两部分的权值正好夹着 $k$。然后先将 $k$ 与左子树合并,再将合并后的树与右子树合并(当然如果你喜欢也可以反过来)。
Rk
。查询 $k$ 的排名。将 $k-1$ 作为关键值分裂出两颗子树。则左子树必然是所有权值小于 $k$ 的点。答案即为左子树大小加 1。然后我们再把两颗子树合并起来即可。
Kth
。查询排名为 $k$ 的数。和 Splay 的差不多,只不过不需要
splay
操作了。我们直接根据子树大小和现排名的关系递归解决即可。实在觉得我懒可以去看上面 Splay 中这一操作的解释。
Pre
和Nxt
。懒了,直接用上面俩操作解决了,不解释。。。当然想要常数小一些的正规写法可以根据 $k-1$ 或 $k$ 分裂子树,然后查询左子树的最大值或者右子树的最小值。
Del
。这个操作也比较好想。我们直接把 Treap 分成左子树,需要删除的节点,右子树,然后把左右子树合并即可。
实际上 FHQ-Treap 的很多操作都可以直接把普通 Treap 的操作搬过来,但是因为有了 Split
和 Merge
两个操作,这些普通的操作被大大简化,而且更加易于理解和记忆。
上传和下方懒标记同样鸽给文艺平衡树。
这里关于这个相同权值放在不同点的方式,我确实很无奈,因为我本来也想改成那种相同权值放在一个点的,但奈何又 T 又 WA 又 UB,心态实在爆炸,再加上这种不同点的写法又很亲民,那种查询有多少个 $k$ 之类的操作其实也很简单,就干脆不写了!(没错我就是鸽中鸽王)
代码:
1 |
|