SA

后缀

我们前面介绍的Border是因为前缀的后缀变为子串,所以我们需要Border

但后缀是什么?我们为什么要用?

不知道。

因为我们是无法描述SA

0xFF 定义+SA定义

下文中的i若为下标则代表数值,否则代表\([i,n]\)的一个后缀


Suffix Array(后缀数组), 它是最难最难的一个算法。

\(sa_k\)为排名为k的后缀\([sa_i,n]\)

我们并不知道为什么我们需要用\(\mathbf{SA}\),所以我们先想我们如何求他

0x21 后缀排序

我们可以直接一个暴力\(sort\),时间为\(O(n\log n \times cmp)\)

其中cmp为比较时间,为\(O(n)\),所以直接暴力\(sort\)\(O(n^2\log n)\)

我们发现在排序体系下,他的时间最短为\(O(n\log_2n)\)

那暴力sort就为\(O(n^2logn)\)的下限吗?

不是!发现cmp可以用二分哈希,从而时间优化成\(O(n\log^2n)\)

代码超短,所以如果时间没有问题就二分哈希。

但模版是卡这个二分哈希的

接下来就是两条路:

(1)继续优化cmp函数

(2)跳出cmp。

很明显按照经验跳出cmp的思维难度较高,那如何优化cmp函数?

用SA

所以接下来的路只能用跳出cmp的方法了

关于O(n)的SA-IS笔者不会,但O(nlogn)的笔者就会了。


我们考虑\(rk_i\),为\(sa_{rk_i}=i\)的数组。

那么我们倍增,然后按照有了二元组\((rk_i,rk_{i+\frac{k}{2}+1})\),然后排序即可

代码有点小长,建议封装。

struct SuffixArray{
    string s;
    ll n;
    vector<ll> sa, rk, h;
    void build(string st){
        s = st;
        n = s.size()-1;
        sa.resize(n+1);
        rk.resize(n+1);
    }
    void buildSA(){
        ll M = 200;
        vector<ll> Tong(M, 0);
        for(ll i = 1; i <= n; i++)Tong[s[i]]++;
        for(ll i = 1; i < M; i++) Tong[i] += Tong[i-1];
        for(ll i = n; i; i--) sa[Tong[s[i]]--] = i;
        for(ll i = 1; i <= n; i++) rk[sa[i]] = rk[sa[i-1]] + (s[sa[i-1]] != s[sa[i]]);
        for(ll p, k = 1; ; k <<=1){
            p = 0;
            vector<ll> id(n+1, 0);
            for(ll i = n; i > n-k; i--) id[++p] = i;
            for(ll i = 1; i <= n; i++)if(sa[i]>k) id[++p]=sa[i]-k;
            vector<ll> Tong(M+1, 0);
            for(ll i = 1; i <= n; i++) Tong[rk[i]]++;
            for(ll i = 1; i <= M; i++) Tong[i] += Tong[i-1];
            for(ll i = n; i; i--) sa[Tong[rk[id[i]]]--] =  id[i];
                auto ork = rk;
            p = 0;
            for(ll i = 1; i <= n; i++){
                rk[sa[i]] = 
                (ork[sa[i]] == ork[sa[i-1]] &&
                    (sa[i-1]+k<=n?ork[sa[i-1]+k]:0) == (sa[i]+k <= n?ork[sa[i]+k]:0))?
                p:++p;
            }
            if(p == n) break;
            M = p;
        }
    }
}SA;

0x22 Height数组+后缀中的子串

我们在后面讲到Z函数时会着重定义lcp,若未知,请跳转到ex-kmp(实则没关系,看一眼lcp定义即可)

我们定义\(h_i=lcp(sa_i,sa_{i+1})\)

他有什么用?

定理2.1:本质不同子串个数为\(\frac{n(n+1)}{2}-\sum_{i=2}^n h_i\)

定理2.2:lcp(\(sa_i\),\(sa_j\))=\(\min_{k \in (i,j]}h_k\)

上文中的子串实际就代表了后缀数据结构拿出子串方式为$\mathbf{lcp} $

那剩下的问题如何做?

ST,Segment Tree,Tree chain partition and Fenwick Tree。

因为SA将string --转化-->segnele

所以剩下为序列问题,而非字符串问题.

posted on 2026-08-11 09:33  秋天的宇宙  阅读(7)  评论(0)    收藏  举报

导航