后缀数组学习笔记

我们用 \(sa[i]\) 代表排名为 \(i\) 的是哪个字符串,\(h[i]\) 为第 \(i\) 个字符串的排名。

普通的排序是 \(O(n^2\log n)\) 的,我们考虑先按照第一个和第二个字符为关键字排序。

我们先对第一个字符排序,然后就得到了几个串的初始,然后再按照第一个+第二个排序。

比如说:

.......
......^
.....^^
....^^^
...^^^^
..^^^^^
.^^^^^^
void SA()
{	
    for(int i=1;i<=n;i++) x[i]=s[i];
    for(int i=1;i<=n;i++) ++c[x[i]]; // 转成数字,然后对应放进桶里
    for(int i=2;i<=m;i++) c[i]+=c[i-1];
    for(int i=n;i>=1;i--) sa[c[x[i]]--]=i; // 按照第一个字母排序,正序倒序都没关系,因为没有第二关键字
    for(int k=1;k<=n;k=k<<1){	
        int num=0;
        // y[i]表示第二关键字排名为i的数,第一关键字的位置
        // 第n-k+1到第n位是没有第二关键字的,所以排名在最前面 
        // 这里y从前往后第二关键字是不断增大的
        for(int i=n-k+1;i<=n;++i) y[++num]=i; // 后面的第二关键字最小
        for(int i=1;i<=n;i++) if(sa[i]>k) y[++num]=sa[i]-k;
        // 能作为第二关键字的,因为我们的第二关键字的下标是>k的,然后排名从小到大
        for(int i=1;i<=m;i++) c[i]=0;
        for(int i=1;i<=n;i++) c[x[i]]++; // 第一关键字加上去
        for(int i=2;i<=m;i++) c[i]+=c[i-1];
        for(int i=n;i>=1;i--) { sa[c[x[y[i]]]--]=y[i]; y[i]=0; }
        // 和开头一样,我们发现开头就相当于把下标当作了第二关键字
        swap(x,y);
        num=1; x[sa[1]]=1;
        for(int i=2;i<=n;i++){	
            if(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k]) x[sa[i]]=num;
            // 如果一样那么排名也一样
            else x[sa[i]]=++num; // 排名为i大于排名为i-1
        }
        if(num==n) break;
        m=num;
    }
    for(int i=1;i<=n;i++)
        printf("%d ",sa[i]);
    printf("\n");
}

妙啊!

LCP

貌似这个很重要,学一下。

\(lcp(i,j)\)\(suff(sa[i])\)\(suff(sa[j])\) 的最长公共前缀。

显而易见 \(lcp(i,j)=lcp(j,i)\)\(lcp(i,i)=n-sa[i]+1\)

1. \(lcp(i,k)=\min(lcp(i,j),lcp(j,k))\)

\(p=\min(lcp(i,j),lcp(j,k))\)

\(lcp(i,j)\)\(lcp(j,k) \ge p\)

\(i,j,k\)\(p\)个字符相等。

也就是 \(lcp(i,k) \ge p\)

\(lcp(i,k)=q>p\)

\(q \ge p+1\)

那么 $i,k $前 \(q\) 个字符相等,矛盾。

所以 \(lcp(i,k)=p\)

\(lcp(i,k)=\min(lcp(i,i+1),lcp(i+1,i+2),\dots)\)

显然。

\(height[i]\)\(lcp(i,i-1)\),那么 \(height[1]=0\)\(lcp(i,k)=\min(height[j])\) \((i+1 \le j \le k)\)

\(h[i]=height[rk[i]]\)\(h[sa[i]]=height[i]\)

\(h\) 用中文说就是第 \(i\) 个字符串和比它字典序小字符串的 \(LCP\)

\(height\) 就是排名。

要证明 \(h[i] \ge h[i-1] - 1\)

\(i\) 为第 \(i\) 个字符串,\(j\) 为第 \(i-1\) 个字符串,\(k\) 为排名比 \(j\) 小的第一个字符串。

所以 \(lcp(j,k)=height[rk[i-1]]\)

然后讨论第 \(k+1\) 个字符串和 \(i\) 的关系。

分类讨论:

  1. \(k\)\(j\)的首字符不同,那么 \(height[rk[i-1]]=0\),所以 \(height[rk[i]] \ge height[rk[i-1]] - 1\),也就是 \(h[i] \ge h[i-1] - 1\)

  2. 第二种情况,\(k\)\(j\) 的首字符相同,那么由于第 \(k+1\) 个字符串就是第 \(k\) 个字符串去掉首字符得到的,第 \(i\) 个字符串也是第 \(j\) 个字符串去掉首字符得到的。因为 \(k < j\),所以 \(k+1 < i\)。那么 \(k+1\) 就是为排名比 \(i\) 小的第一个字符串。所以第 \(k+1\) 个字符串和第 \(i\) 个字符串的最长公共前缀就是 \(height[rk[i-1]] - 1\)。(感性理解,要利用后缀这一性质)

到此为止,第二种情况的证明还没有完,我们可以试想一下,对于比第 \(i\) 个字符串的排名更靠前的那些字符串,\(sa[rank[i]-1]\)\(i\)\(LCP\) 最长。但是我们前面求得,有一个排在 \(i\) 前面的字符串 \(k+1\)\(LCP(rk[i],rk[k+1])=height[rk[i-1]] - 1\)

又因为 \(height[rk[i]]=LCP(i,i-1) \ge LCP(i,k+1)\)

所以 \(height[rk[i]] \ge height[rk[i-1]] - 1\),也即 \(h[i] \ge h[i-1] - 1\)

for1(i,1,n) rk[sa[i]]=i;
int hi=0;
for1(i,1,n){
    if(rk[i]==1){
        hi=0;
        continue;
    }
    if(hi) hi--;
    int j=sa[rk[i]-1];
    while(i+k<=n && j+k<=n && s[i+k]==s[j+k]) k++;
    height[rk[i]]=k;
}

代码比较好理解。

多个串间的最长公共子串。

建出 \(SA\),利用双指针,保证区间内有 \(n\) 种颜色然后 \(st\) 表求 \(height\)

检查字符串是否出现。

两个串扔到 \(SA\) 里,然后找到原串,然后向四边找。

不同子串个数 P2408。

\(SA\),然后,就是给答案加上 \(len[i] - height[i]\)

所有不同子串的总长度。

一样,只不过变成了等差数列求和。

字典序第 \(k\) 大子串。

通过计算不同子串个数,我们从 '\(a\)'~'\(z\)' 枚举,可以用 \(SA\) 吧。

最小表示法。

\(s+s\) 插进去,然后就是路径长度为 \(s\) 的子串,然后贪心走,可以用 \(SA\)

出现次数。

第一次出现的位置。

所有出现的位置。

最短的没有出现的字符串。

两个字符串的最长公共子串。

多个字符串间的最长公共子串。

对任意子串进行 \(O(1)\) 比较。

posted @ 2025-07-09 10:31  wuhupai  阅读(5)  评论(0)    收藏  举报