后缀数组

后缀数组

一些定义

子串

字符串 s 中,截取任意 ij 的一段就是 s 的子串

后缀

后缀 isuffix(i) 表示从 i 开始到结尾的子串

后缀数组 sa

sai 表示排名为 i 的后缀起始位置

排名数组 rk

rki 表示后缀 i 的排名

求后缀数组

直接快排为 O(n2logn) ,不再赘述。

这里记录复杂度为 O(nlog2n)O(nlogn) 的倍增算法

主要思路:

每次对长度为 2k 的子串进行排序,每一次排序都利用上一次长度为 2k1 的子串排名

长度为 2k 的子串可以用两个长度为 2k1 子串排名作为关键字表示

没有第二关键字直接补 0,当排序后所有排名不同时可以提前结束。

即通过倍增,用子串组合出后缀,双关键字排序优于字符串排序,提高效率

基数排序

O(n) 的时间完成双关键字排序。实际上是两次计数排序

回忆一下计数排序:

  • 桶记录次数
  • 求前缀和
  • 从后往前计算排名

运用到这里来:

m 表示字符集大小

idi 表示第二关键字排名为 i 的子串位置

rki 表示以 i 开始的子串排名即第一关键字

按照第二关键字的顺序标号

const int N = 1e5 + 5;
int n, m, t[N], rk[N], sa[N], id[N];
inline void rs() {
    for (int i = 1; i <= m; i++) t[i] = 0;
    for (int i = 1; i <= n; i++) ++t[rk[i]];
    for (int i = 1; i <= m; i++) t[i] += t[i - 1];
    for (int i = n; i >= 1; i--) sa[t[rk[id[i]]]--] = id[i], id[i] = 0;
}

倍增

oldi 表示上一次 i 的排名

int old[N];
inline int EQ(int x, int y, int k)
{ return old[x] == old[y] && old[x + k] == old[y + k]; }
inline void SA() {
    m = 130;
    for (int i = 1; i <= n; i++) rk[i] = a[i], id[i] = i;
    rs();
    for (int k = 1, p; k <= n; k <<= 1) {
        p = 0;
        for (int i = n - k + 1; i <= n; i++) id[++p] = i;
        for (int i = 1; i <= n; i++) if (sa[i] > k) id[++p] = sa[i] - k;
        rs(), memcpy(old, rk, sizeof(rk)), p = 0;
        for (int i = 1; i <= n; i++) rk[sa[i]] = EQ(sa[i], sa[i - 1], k) ? p : ++p;
        if (p == n) break;
        m = p;
    }
}

分段来看:

    m = 130;
    for (int i = 1; i <= n; i++) rk[i] = a[i], id[i] = i;
    rs();

第一次排序。得到初始排名

        p = 0;
        for (int i = n - k + 1; i <= n; i++) id[++p] = i;
        for (int i = 1; i <= n; i++) if (sa[i] > k) id[++p] = sa[i] - k;

按照第二关键字排序。

  • 由于 [nk+1,n] 没有第二关键字,直接按照顺序加入
  • 枚举排名,若 sai>k 说明可以作为别人的第二关键字,加入其对应第一关键字。

按照排名加入,其实已经按照第二关键字排了序

        rs(), memcpy(old, rk, sizeof(rk)), p = 0;
        for (int i = 1; i <= n; i++) rk[sa[i]] = EQ(sa[i], sa[i - 1], k) ? p : ++p;

排序、计算排名。如果两个关键字旧排名都相同,则新排名也相同。

        if (p == n) break;
        m = p;

如果排名互不相同,可提前推出。更改值域范围

code

Luogu 3809

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 1e6 + 5;
int n, m, sa[N], rk[N], id[N], t[N], old[N];
char a[N];
inline void rs() {
    for (int i = 1; i <= m; i++) t[i] = 0;
    for (int i = 1; i <= n; i++) ++t[rk[i]];
    for (int i = 1; i <= m; i++) t[i] += t[i - 1];
    for (int i = n; i >= 1; i--) sa[t[rk[id[i]]]--] = id[i], id[i] = 0;
}
inline int EQ(int x, int y, int k)
{ return old[x] == old[y] && old[x + k] == old[y + k]; }
inline void SA() {
    m = 130;
    for (int i = 1; i <= n; i++) rk[i] = a[i], id[i] = i;
    rs();
    for (int k = 1, p; k <= n; k <<= 1) {
        p = 0;
        for (int i = n - k + 1; i <= n; i++) id[++p] = i;
        for (int i = 1; i <= n; i++) if (sa[i] > k) id[++p] = sa[i] - k;
        rs(), memcpy(old, rk, sizeof(rk)), p = 0;
        for (int i = 1; i <= n; i++) rk[sa[i]] = EQ(sa[i], sa[i - 1], k) ? p : ++p;
        if (p == n) break;
        m = p;
    }
}
void put(int x) { if (x > 9) put(x / 10); putchar(x % 10 | 48); }
int main() {
    scanf("%s", a + 1);
    n = strlen(a + 1);
    SA();
    for (int i = 1; i <= n; i++) put(sa[i]), putchar(32);
}

最长公共前缀

LCP ,此处令 LCP(i,j)suffix(sai)suffix(saj) 的最长公共前缀

有如下几个定义

  • heighti=LCP(i,i1) ,特别地,height1=0

    即排名相邻的后缀的 LCP

  • hi=heightrki ,即 i 和它前一名的 LCP

性质

  • LCP(i,j)=LCP(j,i)LCP(i,i)=nsai+1

  • k[i,j],LCP(i,j)=min(LCP(i,k),LCP(k,j))

    p=min(LCP(i,k),LCP(k,j))

    LCP(i,k)p,LCP(k,j)p

    suffix(sai)=u,suffix(saj)=v,suffix(sak)=w

    u,wp 个字母相同, v,wp 个字母相同

    因为 p=min(LCP(i,k),LCP(k,j)) ,所以 up+1<wp+1wp+1<vp+1 (排名)

    所以 up+1vp+1LCP(i,j)=p

  • LCP(i,j)=mink=i+1jheightk

    证明:用上一个性质

    LCP(i,j)=min(LCP(i,i+1),LCP(i+1,j))=min(LCP(i,i+1),min(LCP(i+1,i+2),LCP(i+2,j))=mink=i+1jLCP(k,k1)=mink=i+1jheightk

求法

还记得 hi=heightrki 吗,它有关键性质:

hihi11

假设后缀 i1 前一名为后缀 j ,分别删去首字母后得到后缀 j+1 和后缀 i

此时后缀 j+1 一定在后缀 i 前面,且二者最长公共前缀为 hi11

由先前的性质 ,可得 hi11 是一些连续 hmin 值,包括了 hi

所以 hihi11

按照从小到大的顺序计算 hi ,暴力后移那么复杂度是 O(n)

实现时:没有必要开出 h 数组,用变量记录,然后赋值 heightrki 即可

int h[N];
// h is height
void get() {
    for (int i = 1, j, k = 0; i <= n; h[rk[i++]] = k)
        for (k ? --k : 0, j = sa[rk[i] - 1]; a[i + k] == a[j + k]; k++);
}

应用

最长公共前缀

询问任意两个后缀的最长公共前缀。

求出 height ,用 RMQ 解决,O(nlogn)

单个字符串

不可重叠最长重复子串

乐曲主题

二分答案,答案变成判定性问题。

height 分为连续的组,如果有连续的一段 heightmid ,且 max(sai)min(sai)>mid

说明存在长度为 mid 且不重叠的重复子串。

height 分组的方法很重要

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 50005;
int n, m = 200, a[N], rk[N], id[N], old[N], sa[N], t[N], h[N];
inline bool EQ(int x, int y, int k) { return old[x] == old[y] && old[x + k] == old[y + k]; }
inline void bui() {
    for (int i = 1; i <= n; i++) ++t[rk[i] = a[i]];
    for (int i = 1; i <= m; i++) t[i] += t[i - 1];
    for (int i = n; i >= 1; i--) sa[t[rk[i]]--] = i;
    for (int k = 1, p; k <= n; m = p, k <<= 1) {
        p = 0;
        for (int i = n - k + 1; i <= n; i++) id[++p] = i;
        for (int i = 1; i <= n; i++) if (sa[i] > k) id[++p] = sa[i] - k;
        for (int i = 1; i <= m; i++) t[i] = 0;
        for (int i = 1; i <= n; i++) ++t[rk[i]];
        for (int i = 1; i <= m; i++) t[i] += t[i - 1];
        for (int i = n; i >= 1; i--) sa[t[rk[id[i]]]--] = id[i], id[i] = 0;
        memcpy(old, rk, sizeof(rk)), p = 0;
        for (int i = 1; i <= n; i++) rk[sa[i]] = EQ(sa[i], sa[i - 1], k) ? p : ++p;
        if (p == n) break;
    }
    for (int i = 1, j, k = 0; i <= n; h[rk[i++]] = k)
        for (k ? --k : 0, j = sa[rk[i] - 1]; a[i + k] == a[j + k]; k++);
}
int chk(int x) {
    int mx = sa[1], mn = sa[1];
    for (int i = 2; i <= n; i++) {
        if (h[i] < x) mx = mn = sa[i];
        else {
            mx = max(mx, sa[i]);
            mn = min(mn, sa[i]);
            if (mx - mn > x) return 1;
        }
    }
    return 0;
}
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    for (int i = 1; i < n; i++) a[i] = 90 + a[i + 1] - a[i];
    a[n--] = 0;
    bui();
    int L = 0, R = n, mid, ans = 0;
    while (L <= R) {
        mid = L + R >> 1;
        if (chk(mid)) ans = mid, L = mid + 1;
        else R = mid - 1;
    }
    printf("%d", ans > 3 ? ans + 1 : 0);
}

子串个数

求不相同的子串的个数

每一个后缀的贡献是 nsai+1heighti

回文子串

字符串反转接在原字符串后面,用 # 分隔。

枚举回文串中心,分长度为奇数、偶数计算,O(nlogn)

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 20005;
int n, mid, m = 200, rk[N], sa[N], id[N], old[N], h[N], t[N], f[N][20], lg[N], ans, St;
char a[N];
inline void rs() {
    for (int i = 1; i <= m; i++) t[i] = 0;
    for (int i = 1; i <= n; i++) ++t[rk[i]];
    for (int i = 1; i <= m; i++) t[i] += t[i - 1];
    for (int i = n; i >= 1; i--) sa[t[rk[id[i]]]--] = id[i], id[i] = 0;
}
inline int EQ(int x, int y, int k)
{ return old[x] == old[y] && old[x + k] == old[y + k]; }
inline void bui() {
    m = 200;
    for (int i = 1; i <= n; i++) rk[i] = a[i], id[i] = i;
    rs();
    for (int k = 1, p; k <= n; m = p, k <<= 1) {
        p = 0;
        for (int i = n - k + 1; i <= n; i++) id[++p] = i;
        for (int i = 1; i <= n; i++) if (sa[i] > k) id[++p] = sa[i] - k;
        rs(), memcpy(old, rk, sizeof(rk)), p = 0;
        for (int i = 1; i <= n; i++) rk[sa[i]] = EQ(sa[i], sa[i - 1], k) ? p : ++p;
        if (p == n) break;
    }
    for (int i = 1, j, k = 0; i <= n; h[rk[i++]] = k)
        for (k ? k-- : 0, j = sa[rk[i] - 1]; a[i + k] == a[j + k]; k++);
}
inline int rmq(int l, int r) {
    if (r == l) return h[l];
    register int k = lg[r - l + 1];
    return min(f[l][k], f[r - (1 << k) + 1][k]);
}
inline int lcp(int i, int j) {
    if (rk[i] > rk[j]) i ^= j ^= i ^= j;
    return rmq(rk[i] + 1, rk[j]); 
}
int main() {
    scanf("%s", a + 1);
    n = strlen(a + 1);
    a[mid = n + 1] = '#';
    for (int i = 1; i <= n; i++) a[mid + i] = a[n - i + 1];
    n = n * 2 + 1, bui();
    for (int i = 1; i <= n; i++) f[i][0] = h[i];
    for (int i = 2; i <= n; i++) lg[i] = lg[i >> 1] + 1;
    for (int j = 1; j <= 15; j++)
        for (int i = 1; i + (1 << j - 1) <= n; i++)
            f[i][j] = min(f[i][j - 1], f[i + (1 << j - 1)][j - 1]);
    for (int i = 1, j, k; i < mid; i++) {
        j = 2 * mid - i, k = lcp(i, j);
        if (k * 2 - 1 > ans || (k * 2 - 1 == ans && i - k + 1 < St))
            ans = k * 2 - 1, St = i - k + 1;
    }
    for (int i = 2, j, k; i < mid; i++) {
        j = 2 * mid - i + 1, k = lcp(i, j);
        if (k * 2 > ans || (k * 2 == ans && i - k < St))
            ans = k * 2, St = i - k;
    }
    for (int i = St; i <= St + ans - 1; i++) putchar(a[i]);
}

重复次数最多的连续重复子串

先咕咕了,不会

两个字符串

公共子串

接起来,用 '#' 分隔,求 height 最大值。

注意判断 saisai1 应在不同串中

长度不小于 k 的公共子串的个数

经典问题,后缀数组 + 单调栈

首先拼起来,求 height ,按照 k 分组。

考虑用每一个 B 串匹配排在它前的 A 串的个数,反过来同理。答案分为两部分

对于同一组每一个 A 串对每一个 B 的最大贡献为 heightk+1

栈记录 Scnt 表示当前的 heightA 串的数量,按照 height 单增

同时用 tot 表示当前答案。

heightiStop 需要维护:讲 totcnt 个已经累加的 S 全部替换为当前的 height

运用了 LCP 是区间最小值的特性。

遇到 B 串,求和。

反过来再算一次。

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 200005;
int n, mid, K, m, sa[N], rk[N], id[N], t[N], old[N], h[N], s[N], cnt[N], top;
char a[N];
inline void rs() {
    for (int i = 1; i <= m; i++) t[i] = 0;
    for (int i = 1; i <= n; i++) ++t[rk[i]];
    for (int i = 1; i <= m; i++) t[i] += t[i - 1];
    for (int i = n; i >= 1; i--) sa[t[rk[id[i]]]--] = id[i], id[i] = 0;
}
inline int EQ(int x, int y, int k)
{ return old[x] == old[y] && old[x + k] == old[y + k]; }
inline void SA() {
    m = 130;
    for (int i = 1; i <= n; i++) rk[i] = a[i], id[i] = i;
    rs();
    for (int k = 1, p; k <= n; k <<= 1) {
        p = 0;
        for (int i = n - k + 1; i <= n; i++) id[++p] = i;
        for (int i = 1; i <= n; i++) if (sa[i] > k) id[++p] = sa[i] - k;
        rs(), memcpy(old, rk, sizeof(rk)), p = 0;
        for (int i = 1; i <= n; i++) rk[sa[i]] = EQ(sa[i], sa[i - 1], k) ? p : ++p;
        if (p == n) break;
        m = p;
    }
    for (int i = 1, j, k = 0; i <= n; h[rk[i++]] = k)
        for (k ? k-- : 0, j = sa[rk[i] - 1]; a[i + k] == a[j + k]; k++);
}
int main() {
    while (scanf("%d", &K), K) {
        scanf("%s", a + 1);
        mid = strlen(a + 1);
        a[++mid] = '#';
        scanf("%s", a + mid + 1);
        n = strlen(a + 1);
        SA();
        top = 0;
        LL ans = 0, c = 0, tot = 0;
        for (int i = 1; i <= n; i++) {
            if (h[i] < K) top = 0, tot = 0;
            else {
                c = 0;
                if (sa[i - 1] < mid) c++, tot += h[i] - K + 1;
                while (top && h[i] <= s[top]) {
                    tot -= cnt[top] * (s[top] - h[i]);
                    c += cnt[top], --top;
                }
                s[++top] = h[i], cnt[top] = c;
                if (sa[i] > mid) ans += tot;
            }
        }
        top = 0, c = tot = 0;
        for (int i = 1; i <= n; i++) {
            if (h[i] < K) top = 0, tot = 0;
            else {
                c = 0;
                if (sa[i - 1] > mid) c++, tot += h[i] - K + 1;
                while (top && h[i] <= s[top]) {
                    tot -= cnt[top] * (s[top] - h[i]);
                    c += cnt[top], --top;
                }
                s[++top] = h[i], cnt[top] = c;
                if (sa[i] < mid) ans += tot;
            }
        }
        printf("%lld\n", ans);
    }
}

多个字符串

也是拼起来,用特殊字符分隔。

求出 height ,二分答案,分组判断。

常见复杂度 O(Knlogn) ,其中 Kncheck 复杂度

本文作者:小蒟蒻laf

本文链接:https://www.cnblogs.com/KonjakLAF/p/16585161.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   小蒟蒻laf  阅读(40)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起