zoukankan      html  css  js  c++  java
  • 【字符串】后缀数组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

  • 相关阅读:
    洛谷—— P2234 [HNOI2002]营业额统计
    BZOJ——3555: [Ctsc2014]企鹅QQ
    CodeVs——T 4919 线段树练习4
    python(35)- 异常处理
    August 29th 2016 Week 36th Monday
    August 28th 2016 Week 36th Sunday
    August 27th 2016 Week 35th Saturday
    August 26th 2016 Week 35th Friday
    August 25th 2016 Week 35th Thursday
    August 24th 2016 Week 35th Wednesday
  • 原文地址:https://www.cnblogs.com/duzhiyuan/p/11938251.html
Copyright © 2011-2022 走看看