一些有点容易忘的基础算法
树链剖分
常用的树链剖分有重链剖分,实链剖分和长链剖分。
长链剖分主要用于部分和深度有关的树形 \(dp\) 的优化,一般采用指针数组实现。
我们说的树链剖分一般指重链剖分,即选择每个点子树最大的儿子。
不难证明从任何一个点到根都只会经过 \(log_n\) 条重链,这也是其复杂度的保证。
可以将每条重链用一个数据结构维护起来,就能做树上操作了。
动态树
动态树是基于实链剖分的数据结构,非常强大,但编码复杂度相对较高。
我使用的是基于 \(splay\) 的动态树。
动态树维护的是若干实链,每个实链用一颗平衡树维护。
动态树的核心操作是 access
,意味将目标点 \(x\)
到根的路径全部打通,并且只包含这条路径。
其它操作简要介绍一下实现:
make_root
:先 access
,然后把 \(x\) splay
到根,然后翻转整颗
\(splay\) ,因为 \(splay\) 外的形态没有改变,所以只要 \(splay\)
内部的形态正确,那么整棵树的形态就正确,如果对于一个 \(splay\)
所有的节点交换了左右儿子,那么就是倒序了这颗 \(splay\) ,\(x\)
又是深度最大的点,所以这样是正确的。
link
:很简单,直接将目标点 \(x\) splay
到当前根,当然,注意到原树之间的关系是 \(splay\) 根节点的关系,\(splay\) 根节点的父亲其实是 \(splay\)
中深度最小的点的父亲,然后改父亲改成 \(y\) 就行。
cut
:假设有一个虚根 \(0\),把 \(x\) make_root
,把 \(y\) access
然后
splay
\(y\),直接双向断开。
1 | void push_up(int rt){} |
虚树:
一般用来处理询问很多但规模不大的树上问题。
点分治
用来处理树上路径的计数问题,特别是路径长度相关
后缀排序
对一个字符串的所有后缀排序,约定 \(sa[i]\) 表示排名为 \(i\) 的后缀的起始位置,约定 \(rk[i]\) 表示起始位置为 \(i\) 的后缀的排名。\(height[i]\) 为排名为 \(i,i-1\) 的后缀的 \(lcp\) 。
先按第一个字母基数排序一遍,然后倍增法。
1 | for(i=1;i<=n;i++)sum[rk[i]=ch[i]]++; |
莫队
莫队是暴力数据结构,将询问离线后,以较低的复杂度移动左右端点,然后处理询问。
设移动端点的复杂度为 \(O(x)\) ,那么莫队复杂度为 \(O(n \sqrt n\times x)\),无法将 \(x\) 放在 \(\sqrt n\) 下面。
常见的卡常技巧有奇偶性排序等。
如果只能支持插入和删除中的一种操作,那么可以使用回滚莫队,拿一个栈记录操作,基于操作的撤销实现插入或删除。
树上莫队和普通莫队区别不大。
(差一个二次离线要补)
拓展KMP
咕咕咕
树哈希
一般来讲,可以这么哈希,再加上树大小的判断,就不会出问题。 \[ f_{now}=1+\sum f_{son(now,i)} \times prime(size_{son(now,i)}) \]
但这个哈希是有错的,可以对最小括号序列哈希。
具体的,一颗无标号有根树按照遍历顺序可以得到一个括号序列,即将子树的括号序列拼起来再套一个括号。
然后考虑对这个括号序列哈希,因为遍历顺序无关,所以要最小括号序列。
实际上可以直接对子树哈希值排序之后,按这个顺序往后写,外面添一层括号,对括号序列(二进制串)哈希。