一、理解:
1、后缀树,举例说明:
eg: 利用字符串prop建立后缀树。
暴力地说,可以三步建树。
第一步,得到prop的所有后缀:
B[0] = “”
B[1] = “p”,第四个后缀
B[2] = “op”,第三个后缀
B[3] = “rop”,第二个后缀
B[4] = “prop”,第一个后缀
以上五个,都是它的后缀
第二步,用这五个后缀建立trie树。
第三步,压缩:
如果一段树枝既没有分支,又没有哪一条后缀结束,它就要压缩成一个点;
如果一段树枝没有分支,但是有一条后缀在其中结束了,就先为结束点建立一个空节点,再让剩余部分压缩成点。
这样就不会把后缀也压缩掉了,压缩完就是后缀树,长这样:
建议自己先画一遍
2.后缀树与后缀数组(SA)和后缀自动机(SAM):虽然三者的前缀相同,但后缀的进化程度倍增,不是同纲。
例如后缀数组:
市面上常见两种写法:倍增(O(nlogn))和DC3(O(n))。
DC3,又称skew,y总说它的常数比倍增大,而且难写,所以一般用倍增。
倍增写法把字符串的所有后缀按字典序排序(基数排序),排完序得到的就是后缀数组SA:
SA[i]:排名第i位的是第几个后缀(不考虑空后缀)
而一般会用到的数组height:
height[i]:sa[i]和sa[i-1]的最长公共前缀
是后缀数组扩展得来的。
并且通过扩展功能,后缀数组可以逐渐向后缀树转化。
3.为什么没见人用后缀树:后缀树功能完整,构造算法也逐渐优化了。
后缀树魔法:
1.查找字符串S1是否在字符串S2中
2.计算字符串a在字符串S中的重复次数
3.查找字符串S中的最长重复子串
4.查找字符串S1和S2的最长公共子序列(LCP)
5.广义后缀树:处理多个字符串的后缀,分别加标记就行
但是功能完整就表示效率不是最优,而且它难写。
后缀树的确炫酷,但是我劝大家学后缀数组。
建树的算法学一个通宵午不一定能学会一个,而学两个小时的后缀数组,就能求LCP
二、前置技能:
基数排序O(n+k):
eg:数组A={9, 7, 5, 5, 4}
第一步,计数:
B[4]=1
B[5]=3
B[7]=4
B[9]=5
B[i]表示小于等于i的数的个数
第二步,遍历数组A并排序:
A[0]=9,排序为B[9]=5,B[9]–
A[1]=7,排序为B[7]=4,B[7]–
A[2]=5,排序为B[5]=3,B[5]–
A[3]=5,排序为B[5]=2,B[5]–
A[4]=4,排序为B[4]=1,B[4]—
注意:A[2]的5排序为3,A[3]的5排序为2。
如果第二步遍历时从后往前遍历,就变成A[2]的5排序为2,A[3]的5排序为3。
lcp的两个性质:
1.lcp[i, j]表示排名第i位的后缀和排名第j位的后缀的lcp。
那么显然有lcp[i, j] = min(lcp[i, k], lcp[k, j]),i≤k≤j。
所以lcp[i, j] = min(lcp[i, i+1], lcp[i+1, i+2], …, lcp[j-1, j])。
2.height[i] = lcp[i-1,i]
定义h[i] = height[ rk[i] ],即第i个后缀的height,即第i个后缀和排名比它小一位的后缀的lcp。
那么有h[i]≥h[i-1]-1。
三、后缀数组
输入一个字符串s,求sa数组和height数组。
先上代码:
#include<bits/stdc++.h>//acwing2715
using namespace std;
const int N=1000010;
int n,m;
char s[N];
int sa[N], x[N], y[N], c[N], rk[N], height[N];
void get_sa(){
//第一轮排序,只按第一个字母排
for(int i=