SA学习笔记
SA算法入门
后缀数组是什么?
以下用\(S_i\)表示字符串S的[i...n]子串部分
对于一个字符串S的所有后缀,即所有 \(S_i\), 按字典序进行排序
我们可以求出两个东西:
一个叫 rank[i], 指\(S_i\)在所有后缀中的排名
一个叫 sa[i] , 指排名为i的后缀的是\(S_{sa[i]}\)
后缀数组怎么求
我们可以考虑使用倍增的思想求解
利用字符串字典序的特性 (先比较高位再比较低位)
具体来说, 求解时先求出长度为 \(len\) 的子串的答案
即: 每个 S[i...min(i+len-1,n)] 的排名与位置的双射(sa与rank)
然后递推出长度为 \(2*len\) 时的情况
比较过程可用基数排序实现
精华代码
int cmp(int x,int y,int w){ return s[x]==s[y] && s[x+w]==s[y+w]; }
void SA()
{
int m=300; //可能出现的排名数量
for(re int i=1;i<=n;++i) cnt[rk[i]=s[i]]++; //先把长度为1的子串算出答案
for(re int i=1;i<=m;++i) cnt[i]+=cnt[i-1];
for(re int i=n;i>=1;--i) sa[cnt[rk[i]]--]=i;
for(re int w=1;;w<<=1)
{
int p=0;
for(re int i=n;i>n-w;i--) id[++p]=i; //基数排序
for(re int i=1;i<=n;++i) if(sa[i]>w) id[++p]=sa[i]-w;
for(re int i=1;i<=m;++i) cnt[i]=0;
for(re int i=1;i<=n;++i) cnt[rk[id[i]]]++;
for(re int i=1;i<=m;++i) cnt[i]+=cnt[i-1];
for(re int i=n;i>=1;--i) sa[cnt[rk[id[i]]]--]=id[i];
p=0; memcpy(od, rk, sizeof(rk));
for(re int i=1;i<=n;++i) rk[sa[i]]=cmp(sa[i],sa[i-1],w) ? p : ++p; //利用sa推出rk
if(p==n) break; m=p;
}
}
height数组
总概
height[i]表示排名第i名的子串与前一名子串的LCP的长度
以下用ht代表height
有了ht数组, 本质上我们得到的是所有后缀的前缀的信息, 而后缀的前缀,就是S中的子串
所以我们可以进行很多问题的处理
求解ht(代码)
先引入一个结论: \(ht[rk[i]]>=ht[rk[i-1]]]-1\)
证明可参考oiwiki
for(re int i=1,j=0;i<=n;++i)
{
if(j) j--;
while(s[i+j]==s[sa[rk[i]-1]+j])++j;
ht[rk[i]]=j;
}
应用
子串的LCP
由定义可知,求给定两个子串的LCP,直接找到以它们为起始位置的后缀,对ht求区间min即可
求不同子串的数目
正难则反,考虑总数减掉重复的子串数目
那么重复的子串,就可以理解成重复的后缀的前缀,也就是排序后每个后缀与上一个的LCP
\(ans=n*(n+1)/2-\sum_{i=2}^nht[i]\)
求两个串(不同/相同)子串数目
先将两个串拼一起,算出长串的答案,再减去两个串各自内部的答案
才会这些,慢慢更吧...
小技巧
求解两个串有关题目
将B串拼在A串后,中间插入一个字符集以外的字符间隔
嗯,就这样了...