后缀数组学习小记

模板题:acwing 2715.后缀数组

题目大意:

给定一个长度为 \(n\) 的字符串 \(S\),求出 \(SA\) 数组和 \(Height\) 数组。
定义从第 \(i\) 个字符开始的后缀编号为 \(i\)
\(SA_i\) 表示排名第i的后缀编号。
\(Height_i\) 表示排名为 \(i\) 的非空后缀与排名为 \(i − 1\) 的非空后缀的最长公共前缀的长度
特别地:规定 \(Height_1 = 0\)

算法流程:

\(SA\) 数组有两种方法,一种是倍增法,\(O(n\log_2 n)\) 的时间复杂度,一种是DC3,\(O(n)\) 的时间复杂度,但常数很大且较复杂。所以常用倍增法。

首先要求 \(SA\) 数组:

\(\texttt{step}\;1\)
将所有后缀按照第一个字符基数排序,相同的保留原有顺序不变,求出当前的 \(SA\) 数组。

\(\texttt{step}\;2\)
假设当前已经按前 \(k\) 个字符排好序,将前 \(k\) 个字符当成第一关键字,第 \(k + 1\)\(2k\) 个字符当成第二关键字排序。
显然,第 \(i\) 个后缀的第二关键字就是第 \(i + k\) 个后缀的第一关键字。

为了满足基数排序条件,可以先将其按第二关键字排序,再按第一关键字基数排序。
\(k\)\(1\) 开始,每次翻倍。

然后求 \(Height\) 数组:
\(rk_i\) 表示第 \(i\) 个后缀的排名。

引理1
\(lcp(i, j)\) 表示第 \(SA_i\) 个后缀和第 \(SA_j\) 个后缀的最长公共前缀长度,当 \(i < j < k\)\(lcp(i, j) = \min(lcp(i, k), lcp(k, j))\)

引理2
\(h_i=Height_{rk_i}\),则有 \(h_i \ge h_{i - 1} - 1\)

证明:
\(SA_{rk_i - 1} = k\),故 \(h_{i - 1} = lcp(i - 1, k)\)
\(S_{i - 1} \ne S_k\),则 \(h_{i - 1} = 0\),故 \(h_i \ge h_{i - 1} - 1 = 0\)
否则,\(lcp(rk_i, rk_{k + 1}) = h_{i - 1} - 1\)\(rk_{k + 1} \le rk_{sa_i - 1} < rk_i\)。由引理1得,\(lcp(rk_i, rk_{k + 1}) = \min(lcp(rk_i, rk_{sa_i - 1}), lcp(rk_{sa_i - 1}, rk_{k + 1}))\)\(h_i = lcp(rk_i, rk_{sa_i - 1}) \ge lcp(rk_i, rk_{k + 1}) = h_{i - 1} - 1\)

根据引理2,求 \(Height_i\) 时可以从 \(Height_{rk_{i - 1}} - 1\) 开始比对,时间复杂度 \(O(n)\)

代码实现:

#include <bits/stdc++.h>
inline int read(){
    int s = 0, f = 0; char ch = getchar();
    while(!isdigit(ch)){if(ch == '-') f = 1; ch = getchar();}
    while(isdigit(ch)) s = s * 10 + ch - 48, ch = getchar();
    return f ? ~s + 1 : s;
}
inline int max(int x, int y){return x > y ? x : y;}
inline int min(int x, int y){return x < y ? x : y;}
const int N = 1e6 + 5;
int n, m;
int sa[N], rk[N], c[N];                                                                                 //sa[i]:排名第i的后缀 rk[i]:第i个后缀的排名
int x[N], y[N];                                                                                         //x[i]:第i个后缀的的第一关键字 y[i]:按第二关键字排序后排名为i的后缀编号
int height[N];
char s[N];
inline int num(char ch){                                                                                //字母转数字,相对顺序不变
    if(isdigit(ch)) return ch - '0' + 1;
    else if(isupper(ch)) return ch - 'A' + 11;
    else return ch - 'a' + 37;
}
inline void get_sa(){
    for(int i = 1; i <= n; ++ i) ++ c[x[i] = num(s[i])];
    for(int i = 2; i <= m; ++ i) c[i] += c[i - 1];
    for(int i = n; i; -- i) sa[c[x[i]] --] = i;                                                         //基数排序,按第一个字符                                                                                                    
    for(int k = 1; k <= n; k <<= 1){                                                                    //k表示第一关键字和第二关键字的长度
        int cnt = 0;
        for(int i = n - k + 1; i <= n; ++ i) y[++ cnt] = i;                                             //后k个后缀第二关键字为0
        for(int i = 1; i <= n; ++ i)
            if(sa[i] > k) y[++ cnt] = sa[i] - k;                                                        //第i-k个后缀的第二关键字为第i个后缀的第一关键字
        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; -- i) sa[c[x[y[i]]] --] = y[i];                                               //再按第一关键字排序
        std::swap(x, y);                                                                                //便于第一关键字x更新
        x[sa[1]] = cnt = 1;
        for(int i = 2; i <= n; ++ i)
            x[sa[i]] = (y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) ? cnt : ++ cnt;   //更新第一关键字
        if(cnt == n) break;                                                                             //已经两两不同就结束
        m = cnt;                                                                                        //不同第一关键字的个数
    }
    for(int i = 1; i <= n; ++ i) rk[sa[i]] = i;
    return;
}
inline void get_height(){
    for(int i = 1, k = 0; i <= n; ++ i){
        if(rk[i] == 1) continue;
        if(k) -- k;
        int j = sa[rk[i] - 1];                                                                          //排名在第i个后缀之前一个的后缀编号
        while(i + k <= n && j + k <= n && s[i + k] == s[j + k]) ++ k;
        height[rk[i]] = k;
    }
    return;
}
int main(){
    scanf("%s", s + 1);
    n = strlen(s + 1), m = 62;
    get_sa();
    get_height();
    for(int i = 1; i <= n; ++ i) printf("%d ", sa[i]);
    puts("");
    for(int i = 1; i <= n; ++ i) printf("%d ", height[i]);
    return 0;
}
posted @ 2023-05-09 16:22  牛肉爱吃dks  阅读(35)  评论(0)    收藏  举报