• 【字符串】后缀数组SA


    后缀数组

    概念

    实际上就是将一个字符串的所有后缀按照字典序排序

    得到了两个数组 (sa[i])(rk[i]),其中 (sa[i]) 表示排名为 i 的后缀,(rk[i]) 表示后缀 i 的排名

    注意到 (rk)(sa) 是互逆的,即 (sa[rk[i]]=rk[sa[i]]=i)

    先讨论几个关于 (lcp) 的性质,令 (lcp(i,j)) 表示 (sa[i])(sa[j]) 的最长公共前缀

    1. (lcp(l,r)=min(lcp(l,i),lcp(i,r)),lle ile r),注意这里只需要枚举任意一个 i 即可

      证明:

      (p=min(lcp(l,i),lcp(i,r)),sa[l]=u,sa[r]=v,sa[i]=w)

      由于 (u)(w) 的前 (p) 位相同,(v)(w) 的前 (p) 位相同

      所以 (u)(v)(lcp) 至少为 (p)

      假设 (u)(v)(lcp>p),不妨设其为 (q=p+k)

      我们知道 (u[q] eq w[q],v[q] eq w[q]),并且 (w[q]ge u[q],v[q]ge w[q])

      那么 (w[q]) 只能是 (>u[q]),且 (v[q]) 只能是 (>w[q]),所以 (u[q]) 不可能等于 (v[q])

    2. (lcp(l,r)=min_{i=l+1}^rlcp(i,i-1))

      证明:

      注意到 (lcp(l,r)=min(lcp(l,l+1),lcp(l+1,r)))

      递归运算即可得到上式

    根据这两个数组我们能得到 (H) 数组,(H[i]) 表示 (sa[i])(sa[i-1 ]) 的最长公共前缀的长度

    (H) 有两个性质

    1. (H[rk[i]]ge H[rk[i-1]]-1)

      证明:

      (suf[k]) 为排名正好在 (suf[i-1]) 前一个的后缀,那么它们的公共前缀的长度就是 (H[rk[i-1]])

      注意到 (su f[k+1]) 的排名一定在 (suf[i]) 之前,而 (suf[k+1])(suf[i]) 的最长公共前缀至少是 (H[rk[i-1]]-1)

      所以 (H[rk[i]]ge H[rk[i-1]] - 1)

    2. 后缀 (x)(y) 的最长公共前缀为 (min_{i=rk[x]+1}^{rk[y]}H[i]),实际上这就是 (lcp) 的第二个性质

    3. (sa[i])(sa[1])(sa[i-1]) 的最长公共前缀是与 (sa[i-1]) 的最长公共前缀

    实际应用中求 (sa) 的方法就是倍增了 = =,具体实现原理就不写了 = =

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #define maxn 1000010
    using namespace std;
    
    int n, m;
    
    char s[maxn];
    
    int tax[maxn], tp[maxn], sa[maxn], rk[maxn], M = 122; 
    void rsort() { // tp[i] 表示排序时第二关键字为 i 的后缀是什么
        // rsort 的实际作用就是按照 rk[i] 为第一关键字,tp[i] 为第二关键字从小到大排序
        for (int i = 0; i <= M; ++i) tax[i] = 0;
        for (int i = 1; i <= n; ++i) ++tax[rk[i]];
        for (int i = 1; i <= M; ++i) tax[i] += tax[i - 1];
        for (int i = n; i; --i) sa[tax[rk[tp[i]]]--] = tp[i];  
    }
    
    int c1, H[maxn]; // c1 表示不同的 rk[i] 有多少个,当 c1 == n 时,排序完毕
    void SA() {
        if (n == 1) return (void) (sa[1] = rk[1] = 1); 
        for (int i = 1; i <= n; ++i) rk[i] = s[i], tp[i] = i; rsort();
        for (int k = 1; k < n; k *= 2) { // k 是已经排序完毕的长度
            if (c1 == n) break; M = c1; c1 = 0; // c1 清零之后就暂时当做计数器用了
            for (int i = n - k + 1; i <= n; ++i) tp[++c1] = i;
            for (int i = 1; i <= n; ++i) if (sa[i] > k) tp[++c1] = sa[i] - k; // 更新 tp 数组
            rsort(); // 在进行这个 rsort 之前,sa 和 rk 排序的长度依旧是 k
            // rsort 结束之后,sa 得到了更新,所以下面的操作是更新 rk
            swap(rk, tp); rk[sa[1]] = c1 = 1; // 这时候的 tp 变成了上一轮的 rk,c1 是计数器同时是不同的 rk[i] 的个数
            for (int i = 2; i <= n; ++i) {
                if (tp[sa[i - 1]] != tp[sa[i]] || tp[sa[i - 1] + k] != tp[sa[i] + k]) ++c1;
                // 如果排名为 i - 1 和排名为 i 的串的前 k 位相同,并且前 2k 位也相同,那么这两个串的排名就暂时相同了
                rk[sa[i]] = c1;
            }
        } int lcp = 0;
        for (int i = 1; i <= n; ++i) {
            if (lcp) --lcp;
            int j = sa[rk[i] - 1];
            while (s[j + lcp] == s[i + lcp]) ++lcp;
            H[rk[i]] = lcp; 
        }
    }
    
    int main() {
        scanf("%s", s + 1); n = strlen(s + 1); SA();
        for (int i = 1; i <= n; ++i) printf("%d ", sa[i]); 
        return 0; 
    }
    
    

    应用

    单个字符串

    1. 可重叠最长重复子串

      定义:对于一个串,如果它的一个子串在原串中出现出现至少两次,则称这个子串是一个重复子串

      可重叠指两次出现的位置可以重叠

      直接求 (H) 的最大值即可

      证明:同理,一个子串一定是某一个后缀的前缀

      所以原问题相当于求两个后缀的最长公共前缀的最大值,这个东西显然就是 (H) 的最大值

    2. 可重叠重复子串计数

      求有多少本质不同的子串出现至少两次

      再次重申,子串 = 某一个后缀的一个前缀

      我们考虑将所有后缀按照字典序加入,我们求没加入一个后缀对答案贡献多少

      首先他所贡献的重复的子串为 (H[i]),但是有一些已经被记过数了

      所以我们考虑去重

      以下开始胡扯 不知道给怎么说了

      给个答案吧 (ans=sum_{i=1}^nmax(H[i]-H[i-1],0))

      不过讲道理,这个东西 (SAM) 随便做

    3. 不可重叠最长重复子串

      注意到 (H) 这个东西是极大的,考虑二分答案 x

      那么我们考虑如果固定一个 (sa[i]),那么排名在 (sa[i ]) 前面的,且与 (sa[i]) 的最长公共前缀的大小至少为 (x),且与 (sa[i]) 的位置相差至少为 (x)(要不然就重合了

      假设这个串为 (sa[j]),注意到因为 (H) 这个东西是极大的,所以 (sa[i])(sa[j+1])(sa[i-1]) 这些串的最长公共前缀都至少为 (x),实际上 (H[j+1])(H[i]) 都至少为 (x)

      upd:那么反过来就是如果 (H[j+1])(H[i]) 都大于等于 (x),那么 (sa[i])(sa[j])(lcp) 至少为 (x)

      这启示我们将 (H) 按照二分的 (x) 分组,也就是说 (ge x) 且连续的 (H) 分一组,如果某一组里的后缀的位置的最大值 - 最小值 (ge x),则表示二分的这个值合法

    4. 可重叠的 (k) 次最长子串

      同样二分答案,只不过判断的是否有一个组的个数大于等于 (k)

      按照 3 的思路,我们发现只需要求每一个 (H) 的每一个 (k-1) 区间的最小值即可

      显然可以单调队列维护

    5. 本质不同的子串的个数

    实际上就是求所有后缀有多少本质不同的前缀

    我们考虑按照将所有后缀按照字典序排序,那么每次新加进来的一个后缀的前缀的个数为 (n-sa[i]+1),但是与前面的所有后缀重复的前缀有 (H[i]) 个,因为 (H) 是极大的

    答案即为 (sum_{i=1}^nn-sa[i]+1-H[i])

    1. 重复出现次数最多的连续重复子串

      我们考虑枚举连续重复子串的长度 (l),然后我们将串分成 (lfloorfrac{n}{l} floor)

      求出相邻两块的 (lcp) 的长度 (k),那么出现次数为 (k/l+1)

      注意还要检查一下块前面的一部分是否可以加进去

      时间复杂度为 (O(nlog n))

    两个字符串

    1. 两个串的最长公共子串

      将两个串之间用没有出现过的字符拼接起来

      然后直接后缀排序,注意到不会有任何一个 (H[i]) 能够跨越未知字符

      如果 (sa[i])(sa[i-1]) 不是同一个串的后缀,那么可以拿 (H[i]) 来更新答案

      容易知道最大值一定是取在某个 (sa[i])(sa[i-1])

      upd:为啥我现在不知道了

      还是随便口胡一下吧

      因为至少有一个 (sa[i])(sa[i-1]) 是分别属于不同串的后缀的

      如果只有一个的话,那么这个 (H[i]) 就是答案,因为 (H) 是极大的

      如果有多个的话,就取最大值好了

      或者换个说法如果最终答案是 (sa[i])(sa[j])(lcp),且 (i)(j) 不相邻

      那么 (H[j+1])(H[i]) 都是至少有这么大的,中间一定有一对 (sa[k])(sa[k+1]) 不属于同一个串的后缀

    2. 长度不小于 (k) 的公共子串个数

    题目列表:

    LuoguP2408

    LuoguP2852

    LuoguP4051

    SP1811

    LuoguP4248

  • 相关阅读:
    Javascript中怎么定义类(私有成员、静态成员)?
    Web前端国内的叫法与行业归类吗
    CSS hack,CSS简写,CSS定义应注意的几个问题
    7个在IE和Firefox中不同的JavaScript语法
    IE和Firefox中的事件
    IE8的css hack /9
    CSS hack
    运行,复制,保存,runCode,copyCode,saveCode,运行代码框
    自由使用层的叠加
    WordPress自定义URL的Rewrite规则
  • 原文地址:https://www.cnblogs.com/duzhiyuan/p/11938251.html
Copyright © 2020-2023  润新知