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串后,中间插入一个字符集以外的字符间隔

posted @ 2021-09-10 20:26  yzhx  阅读(77)  评论(0编辑  收藏  举报