支持子树修改的全局平衡二叉树!

· · 算法·理论

重写,因为原文感觉没人能看懂。

不是靠标记永久化实现的。

下文中叶子指没有左右儿子的,可能有轻儿子,不是真的叶子。

1. 三度化

考虑将原树转换成二叉树,具体的,每有一个轻儿子就建一个虚点,父亲是上一个虚点,没有就是父亲的实点,重儿子是下一个虚点,没有就没有,轻儿子是原来的轻儿子。

父亲现在的轻儿子就是第一个虚点。

大致长这样

这样就有两个很好的性质:

  1. 每个实点对应的虚点都是一条重链。

  2. 每个点只有一个轻儿子。

2. 建全局平衡二叉树

可以看 oiwiki。

如题,在这步,我们不考虑虚点和实点的区别,同时,我们需要在父亲处维护轻儿子的重链对应的全局平衡二叉树的根,也就是认子。

然后如果遇到子树修改,暴力 push_down,push_up 即可,因为只有一个轻儿子。

自顶向下

考虑先将这个东西转成 leaf 的,这里推荐 这篇好文

然后使用神秘结论,任何一段没有跨重链的链,其的轻子树 dfs 序也是一个区间。

但是有一个问题,dfs 序小的节点对应的轻子树 dfs 序反而更大,可以考虑翻转一下 dfs 序。

于是,我们就可以判断链的左右端点是在左节点和其轻子树还是在右节点和其轻子树。

如果查询的链就是目前节点对应的重链,那就直接操作。

如果目前节点是叶子节点,那就算上这个点,向轻儿子递归。

如果都在左边或都在右边,直接递归。

反之从中间根据 mid 劈开递归。

这个复杂度看起来有点错,但是实际上是对的。

考虑设 F(x) 表示总大小为 x 时的复杂度,G(x) 表示要求链的一个端点在界点的复杂度。

然后就可以列出

F(x)=max(F(x/2)+1,2G(x/2)) G(x)=G(x/2)+1

显然是一个老哥的。

于是,我们就可以在不算 lca,不对 lca 所在的重链做特判的情况下实现链改,吗?

三度化的问题

你直接写上述代码会 wa,原因是因为三度化,可能在 lca 处取到虚点,所以必须算一个 lca,把对应的实点补上。

但是其实如果你发现现在在虚点上递归,且链的左右端点分别在左右端(将要劈开),且都是实点(还没有被劈开),则将对应的实点加入,这个就是对的,因为在虚点上的劈开最多只会有一次。

于是,我们成功再次消灭了 lca 。

子树修改

考虑直接将点到对应重链底和其对应的轻子树修改,我们发现这就是这个子树。

组合意义

这个东西可以很好的处理边,考虑令非叶子节点代表 mid 到 mid+1 的这条边,叶子节点代表到轻儿子的边,你用一个类平衡树过程就可以做了。

我们可以发现,如果我们让虚点建的重链的非叶子节点代表对应的实点,叶子节点代表边,它跟实点的重链的区别就是做了一个点和边的转置。

更诡异的,我们可以认为一个点周边有很多条边等价于一个通过点连接的边的链,不过点不同于边,它自动去重。

应用

这个东西应该是不弱于 top tree 的,但是比 top tree 好写一倍(指某题这个的代码长度是 top tree 的一半),但是也慢一倍。

不过肯定比树剖快。

子树信息查询有可能需要按照虚实点分讨,但是模板树链剖分等不需要。

这个东西维护树上邻域就是正常的考虑上界点和下界点的邻域,合并。

例题

一道绝世好题,std。

所有静态且在树上的 top tree 可以做的题。

本来 top tree 做太石现在可以做的题(如某乘凉州题,提交记录,虽然因为我的常数有点大没能夺得最优解,但强制在线已经赢麻了,且在格式化后跟树剖做法长度几乎相同)

代码

下述代码采用了一个十分神奇的建树方式,就是将重链对应的区间和轻子树对应的区间直接传进去,如果重链上只有一个点就递归建轻子树,反之就正常分治。

代码的前置科技

采用了完全无到儿子的指针的写法,跟前置科技说的 dfs 序线段树差不多。

采用了很多前置科技提到的写法,可能需要学习一下。

这里有一个非常 modern c++ 的模板树链剖分的代码,不长,也就 150 行。

模板树链剖分的代码

这里有一个 p6666 的不完全管的代码