后缀树
万恶的纸糊串 /fn。
参考文章 EA-炫酷后缀树魔术,OldDriverTree-题解:P4143 采集矿石。
后缀树,即考虑将一个字符串的所有后缀插入 trie 中。
懒得画图了 /kk。
将 abcbc 插入 trie 中。
rt
a/ b| \c
O O O
b| c| b|
O O O
c| b| c|
O O O
b| c|
O O
c|
O
发现有很多后缀作为其他后缀的前缀在 trie 上没有显现(如 bc),很多文章考虑在字符串最后加上一个特殊字符如 & 来避免这种问题,但是笔者还没想明白其必要性。
加入终止符后的后缀树。
rt
a/ b| \c
O O O
b| c|\& b|\&
O O O O O
c| b| c|
O O O
b| c| &|
O O O
c| &|
O O
&|
O
但是这样发现节点数是 \(O(n^2)\) 的,应用空间显然不大。考虑减少节点数,将只有一个子节点的节点压缩。
压缩后的后缀树。
rt
a/ b| \c
O O O
b| c|\& b|\&
c| b| O c| O
b| c| &|
c| &| O
&| O
O
这样压缩后节点量级是 \(O(n)\) 级别,准确上界是 \(2n\)。
假设当前后缀插入后节点个数增加,则该后缀从新建节点处往后均没有在后缀树内出现,故最终将缩成一点。
而新建节点时有可能使得原本压缩后的某个节点子节点数 \(>1\) 需要将该点分裂出来。
例:将 abcd& 插入。
rt
a/ b| \c
O O O
b| c|\& b|\&
c| b| O c| O
O c| &|
d/ b| &| O
&/ c| O
O &|
O
接下来考虑后缀树具有什么性质。
· 每个叶子节点到根的路径都是原字符串的一个后缀。后缀树的 dfs 序即为 SA。
· 并且每个点都对应着原串的一个子串(包含被压缩的点),同样的,若将压缩的点全部计入 dfs 序,每个点的 dfs 序则对应其代表子串在所有子串中的排名。
· 后缀树上两个点的 lca 代表该两点代表的子串的 LCP。
构建后缀树。
Ukkonen 太困难了,没看懂,咕咕咕了 /hsh。
根据如上几条性质,发现建树过程即为按后缀排序的顺序插入后缀,将其与上一个插入的后缀连向两者 LCP。且每次插入后缀必定在后缀树的最右链上拓展。
先求出 SA。
考虑类似笛卡尔树用栈维护最右链。
O
\a
O
\b
\c
O
假设当前最右链为上图,考虑插入 abd,因为插入后原本未压缩时 b 连向的节点子节点个数 >1。则需要在 bc 这条边上分裂,再拓展出 d 边,最右链变为 abd。
O
\a
O
\b
O
c| \d
O O
这个分裂过程听起来似乎很麻烦,但实际上只需要维护每个节点到根对应的子串的长度 \(pos\)。随后差分就可以得到每条边上的元素。
而维护最右链,每次找到当前后缀从哪开始拓展,即为将所有栈内 \(pos>len(LCP(sa_i,sa_{i-1}))\) 的节点弹出,而不等式右边即为 SA 中的 height 数组可以 \(O(n)\) 求解。
随后若当前栈顶节点 \(pos=height_i\) 则无需上述分裂过程,栈顶所对节点对应的子串即为 \(LCP(sa_i,sa_{i-1})\),直接新建当前后缀的节点 \(u,pos_u=n-sa_i+1\) 加入最右链即可。
否则 \(LCP(sa_i,sa_{i-1})\) 不存在,需要新加入 \(LCP(sa_i,sa_{i-1})\) 以及当前后缀,将原本弹出的右链部分接到 \(LCP\) 上。
void build(){
get();//求 SA
vector<int>q;
//记录所有节点的顺序,保证最后连边后遍历的顺序是按字典序
z[tp=1]=0,tot=0;//根节点
for(int i=1;i<=n;i++){
while(pos[z[tp]]>h[i])--tp;//将 pos>height_i 的节点弹出
if(h[i]^pos[z[tp]])//不存在 LCP
fa[z[tp+1]][0]=++tot,pos[tot]=h[i],fa[tot][0]=z[tp],
//新建 LCP,将原本弹出部分接到 LCP 上
fa[i][0]=tot,pos[i]=n-sa[i]+1,z[++tp]=tot,z[++tp]=i,q.pb(tot),q.pb(i);
//新建当前后缀维护最右链
else //存在 LCP
fa[i][0]=z[tp],z[++tp]=i,pos[i]=n-sa[i]+1,q.pb(i);
//新建当前后缀维护最右链
}for(int i:q)e[fa[i][0]].pb(i);//建树
}
因为按照这种方式建树应当是保证每个后缀都有一个节点 \(rk_i\) 代表,所以终止符在有没有必要性 /yun。
应用
查询字符串 \(s\) 中 \(t\) 的出现次数。
建出后缀树后,走到代表 \(t\) 的节点 \(u\) 处(不存在则出现次数为 0),则出现次数为 \(u\) 子树内的后缀个数(每次出现必定是某个后缀的前缀且每个后缀至多对应一次出现)。
对于每个左端点 \(l\),右端点 \(r\) 不断增大,则 \(val\) 递增,但降序排名递减,所以每个左端点仅对应一个合法右端点。
考虑找到每个左端点对应的右端点,建立后缀树后在这个 \(s:[l,n]\) 这个后缀对应节点到根倍增找到最后一个 \(val\ge rank\) 的祖先 \(u\),然后在 \(fa_u\to u\) 这条边上继续倍增找到合法右端点(因为可能有几个右端点被压缩在这条边上)。

浙公网安备 33010602011771号