后缀数组(模板)

介绍

后缀数组就是字符串的每个后缀的排序。
主要有两个sa和rk两个数组,sa[i]代表第i大的后缀的位置,rk[i]代表位置i的后缀的排位。满足rk[sa[i]] = sa[rk[i]] = i

实现

有很多求后缀数组的方法,其中一种是倍增法。
先给字符串每一位排序,然后倍增排序。假设当前倍增长度为\(2^k\),那么对于位置i,以rk[i]为第一关键字,rk[i+\(2^k\)]为第二关键字排序。
时间复杂度O(n(logn)^2)。

偷个oiwiki的图,倍增排序示意图:

还有O(n)的复杂度的方法,有机会再补了。

const int N = 2e6 + 10;
typedef long long ll;
//倍增要开两倍空间,每次排序的格式化也要格式化两倍空间(重要)
// 字符串下标从 0 开始;sa、rk从 1 开始
int sa[N], pos[N], rk[N << 1], oldrk[N << 1]; 
char s[N];

void solve(char s[]) {
    int n = strlen(s);
    for(int i = 0; i < n; i++) {
        pos[i + 1] = i;
        cnt[i] = sa[i] = rk[i] = rk[i + n] = ht[i] = 0;
    }
    sort(pos + 1, pos + 1 + n, [s](int x, int y) {return s[x] < s[y];});
    int rnk = 1;
    for(int i = 1; i <= n; i++) {
        if(i > 1 && s[pos[i]] > s[pos[i - 1]]) rnk++;
        rk[pos[i] + 1] = rnk;
    }
    for(int w = 1; w < n; w <<= 1) {
        for(int i = 1; i <= n; i++) sa[i] = i;
        sort(sa + 1, sa + n + 1, [w](int x, int y) {return rk[x] == rk[y] ? rk[x + w] < rk[y + w] : rk[x] < rk[y];});
        for(int i = 0; i <= (n << 1); i++) oldrk[i] = rk[i]; //两倍空间,拷贝要完整地拷贝
        int p = 0;
        for(int i = 1; i <= n; i++) {
            if(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]) {
                rk[sa[i]] = p;
            } else {
                rk[sa[i]] = ++p;
            }
        }
    }
    
    // 注意这里s下标从0开始,while内部要减1
    int k = 0;
    for(int i = 1; i <= n; i++) {
        if(k) k--;
        while(i + k < n && s[i + k - 1] == s[sa[rk[i] - 1] + k - 1]) k++;
        ht[rk[i]] = k;
    }

}
// rk、sa从1开始
// px[i] = rk[id[i]](用于排序的数组所以叫 px)
const int N = 1e6 + 10;
int n, sa[N], rk[N], oldrk[N << 1], id[N], px[N], cnt[N], pos[N], ht[N];
char s[N];

bool cmp(int x, int y, int w) {
  return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w];
}

void solve(char s[]) {
    int n = strlen(s);
    for(int i = 0; i < n; i++) pos[i + 1] = i;
    sort(pos + 1, pos + 1 + n, [s](int x, int y) {return s[x] < s[y];});
    int rnk = 1;
    for(int i = 1; i <= n; i++) {
        if(i > 1 && s[pos[i]] > s[pos[i - 1]]) rnk++;
        rk[pos[i] + 1] = rnk;
    }
    int m = n, p; // m为值域,可以改小
    for(int i = 1; i <= m; i++) cnt[i] = 0;
    for(int i = 1; i <= n; i++) ++cnt[rk[i]];
    for(int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
    for(int i = n; i >= 1; i--) sa[cnt[rk[i]]--] = i;
    // m=p 就是优化计数排序值域
    for(int w = 1;;w <<= 1, m = p) {
        p = 0;
        for(int i = n; i > n - w; --i) id[++p] = i;
        for(int i = 1; i <= n; ++i)
            if(sa[i] > w) id[++p] = sa[i] - w;
        for(int i = 1; i <= m; i++) cnt[i] = 0;
        for(int i = 1; i <= n; i++) ++cnt[px[i] = rk[id[i]]];
        for(int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
        for(int i = n; i >= 1; i--) sa[cnt[px[i]]--] = id[i];
        for(int i = 1; i <= (n << 1); i++) oldrk[i] = rk[i];
        p = 0;
        for(int i = 1; i <= n; i++)
            rk[sa[i]] = cmp(sa[i], sa[i - 1], w) ? p : ++p;
        if(p == n) {
            for(int i = 1; i <= n; i++) sa[rk[i]] = i;
            break;
        }
    }
}

height数组

\(height[i] = lcp(sa[i], sa[i-1])\), 即第i名的后缀与它前一名的后缀的最长公共前缀。

具体就是使用引理\(height[rk[i]] \le height[rk[i-1]]-1\)来暴力求,时间复杂度O(n)。

// 注意这里s下标从0开始,while内部要减1
int k = 0;
for(int i = 1; i <= n; i++) {
    if(k) k--;
    while(i + k < n && s[i + k - 1] == s[sa[rk[i] - 1] + k - 1]) k++;
    ht[rk[i]] = k;
}

参考

posted @ 2020-07-16 19:23  limil  阅读(121)  评论(0编辑  收藏  举报