后缀平衡树学习笔记
后缀平衡树简介
后缀平衡树是一种动态维护后缀排序的数据结构。
具体而言,它支持在串\(S\)的开头添加/删除一个字符。
前置知识-重量平衡树
重量平衡树保证操作影响的最大子树大小是最坏的或均摊的或期望的\(O(logn)\)。
不采用旋转机制的重量平衡树-替罪羊树
替罪羊树依赖于一种暴力重构的操作。
它规定了一个平衡因子\(\alpha\),需要保证对于每个节点有\(\alpha \times sz_x \ge sz_{ls_x},sz_{rs_x}\);
插入节点时,我们每次找出往上最高的不满足平衡条件的节点,将其重构为完全二叉树。
const double alpha=0.7;
int rt,tot,s[2][N],sz[N],cal[N],top;
void build(int &i,int L,int R){
if(L>R)return;int M=(L+R)/2;i=cal[M];sz[i]=R-L+1;
build(s[0][i],l,mid,L,M-1);build(s[1][i],mid,r,M+1,R);
}
void recycle(int&i){
if(s[0][i])recycle(s[0][i]);cal[++top]=i;if(s[1][i])recycle(s[1][i]);i=0;
}
void rebuild(int &i){top=0;recycle(i);build(i,1,top);}
void insert(int &i,int val,int f){
if(!i){i=newnode();v[i]=val;sz[i]=1;return;}
sz[i]++;int fg=f;
if(val<v[i])fg|=(alpha*sz[i]<=(sz[ls[i]]+1)),insert(s[0][i],val,fg);
else fg|=(alpha*szu)rebuild(i);
}
删除节点时,这里使用的是merge左右子树。
为什么?因为zsy聚聚是这么写的啊
瞎遍一下复杂度:即使删除后不满足平衡条件,只要不做插入操作树高也不会高于\(O(logn)\),
而进行插入操作我们就会重构子树。
应该不会\(T\)...
inline void update(int i){sz[i]=sz[s[0][i]]+1+sz[s[1][i]];}
int merge(int a,int b){
if(!a||!b)return a|b;
if(sz[a]>sz[b])return s[1][a]=merge(s[1][a],b),update(a),a;
else return s[0][b]=merge(a,s[0][b]),update(b),b;
}
inline void del(int &i,int p){//删除节点p
if(i==p){i=merge(s[0][i],s[1][i]);return;}
sz[i]--;val[p]<val[i]?del(s[0][i],p):del(s[1][i],p);
}
重量平衡树的一个应用:序列顺序维护问题
给出一个节点序列,要求支持如下两种操作:
- 在\(x\)后插入新节点\(y\)。
- 询问\(a,b\)的前后关系。
平衡树模板题
我们考虑将节点\(x\)映射到实数\(\varphi(x)\),\(\varphi\)的大小关系分节点的顺序关系。
建立平衡树时,我们维护每棵子树\(\varphi\)的取值区间\((l,r)\),规定根节点\(\varphi\)的取值区间为\((0,1)\)。
假设当前节点维护的区间为\((l,r)\),那么这个节点的\(\varphi\)可以当做\(\frac{l+r}{2}\),
左右儿子的区间分别为\((l,\frac{l+r}{2})\)和\((\frac{l+r}{2},r)\)。
做完了?
我们发现\(\varphi\)的分母为\(2^{deep-1}\),而深度太深就会掉精度。
虽然我们知道平衡树的深度是\(O(nlogn)\)的,
但这意味着当我们维护平衡树的平衡时我们需要重新维护子树内所有节点的\(\varphi\)值。
这时重量平衡树就派上了用场。
因为重量平衡树每次影响的子树大小是\(O(nlogn)\)的,因此可以使用于此问题。
这样我们做到了插入\(O(logn)\)查询\(O(1)\)。
后缀平衡树的构造
在\(S\)的前端插入字符\(c\),相当于加入了一个新后缀\(cS\)。
在平衡树上单点插入,考虑如何比较这个新后缀和其他后缀的顺序关系。
哈希
一个简单粗暴的想法是二哈(二分+哈希)求\(lcp\)之后进行判断,
因为需要比较\(O(logn)\)次,所以这样做的复杂度为\(O(log^2n)\)。
套用序列顺序问题
假设我们要拿\(cS\)这个新后缀和一个后缀\(T\)做比较。
一个很有用的条件是后缀\(S\)的排名是已知的。
假设\(T\)可以表示为\(c'T'\),那么我们相当于比较两个二元组\((c,S),(c',T')\)的大小。
比较两个字符的时间当然是\(O(1)\)。
由于\(S\)和\(T'\)的排名都是已经维护好的,因此比较这两个后缀的时间也是\(O(1)\)。
因此我们将比较的时间优化到了\(O(1)\),那么插入的时间复杂度变为\(O(logn)\)。
这种方法无论是在编程复杂度还是时间复杂度上都优于二分+哈希。
后缀平衡树的应用
字符串匹配
给定\(S\)和数个\(T\),每次询问\(T\)在\(S\)中出现了几次。
因为已经后缀排序,只要找到第一个严格小于\(T\)的最后一个后缀和严格大于\(T\)的第一个后缀即可。
匹配时直接暴力。总复杂度为\(O((|S|+\sum|T|)log|S|)\)。
求本质不同子串数
查询前驱/后继,维护子树和即可。
如果只是求这个的话,直接set维护即可,根本不需要用到后缀平衡树了。
STRQUERY by 陈立杰
论文题。
给定一个字符串\(S\),现在要求支持前端,后端,正中间,插入/删除,
以及询问一个串\(T\)在\(S\)中的出现次数。
我们知道:
- 一个后缀平衡树只能支持一边插入/删除。
- 两个平衡树就可以支持两边插入/删除。
其中一个删完后将另外一个分成两份,可以保证重构的代价\(\le\)操作次数。 - 四个平衡树就可以支持前端,后端,正中间,插入/删除。
对于正中间的位置将字符左右弹一下即可。
对于串的连接部分,可以知道长度最多为\(2(|T|-1)\),因此直接抠下来\(kmp\)即可。
时间复杂度为\(O((|S|+\sum|T|)log|S|)\)。
参考资料:《重量平衡树与后缀平衡树在信息学竞赛中的应用》