zoukankan      html  css  js  c++  java
  • [后缀数组][学习笔记]

    定义——来自百度百科

    子串
    一个字符串中连续的一段成为这个字符串的子串。
    后缀
    后缀是指从某个位置 i 开始到整个串末尾结束的一个特殊子串。字符串 r 的从 第 i 个字符开始的后缀表示为 Suffix(i) ,也就是Suffix(i)=r[i,len(r)] 。
    子串的大小
    大小比较:关于字符串的大小比较,是指通常所说的“字典顺序”比较,也就是对于两个字符串u、v,令i 从1 开始顺次比较u[i]和v[i],如果u[i]=v[i]则令 i 加1,否则若u[i]<v[i]则认为u<v,u[i]>v[i]则认为u>v(也就是v<u),比较结束。如果i>len(u)或者i>len(v)仍比较不出结果,那么若len(u)<len(v) 则认为u<v ,若len(u)=len(v) 则认为u=v ,若len(u)>len(v)则u>v。
    从字符串的大小比较的定义来看,S 的两个开头位置不同的后缀u 和v 进行比较的结果不可能是相等,因为u=v 的必要条件len(u)=len(v)在这里不可能满足。
    后缀数组
    在后缀数组中,每个后缀我们都用它的起始位置来表示。sa[x]表示排名为x的后缀是哪一个(里面存的就是这个后缀的起始位置)。
    名次数组
    名次数组是与后缀数组对应的,rk[i]表示i这个后缀的排名。

    算法

    那如何求后缀数组呢,也就是对这n(字符串长度)个后缀排序?
    暴力做法就是对所有的后缀进行排序。这样复杂度就成了(O(n^2log(n)))
    求后缀数组一般使用倍增算法。
    那么怎么倍增呢。
    考虑我们暴力排序的时候,其实就是先比较每个后缀的第一位,但是可能会有第一位相同的,这时候再去比较第二位。依次类推。
    假设我们现在已经对第一位排好序了,然后我们需要去比较第二位。好了,我们把前两位都比较完了,还是有不能区分出名次的后缀,这时候去比较第三位???不不不
    这时候我们发现,我们要继续比较的第3位和第4位其实是其他的后缀的前两位,也就是说我们之前的时候已经获得他们的排名了。那么何必重新比较呢。继续利用之前比较的不是很好吗。
    来张图

    图片还是来自百度百科,懒的画图
    如图可以看到,对于每次排序的时候都有两个关键字,第一个关键字是之前排序的结果,第二个是我们在倍增之后获得的序号。在按照第二关键字排序的时候一定要在第一关键字的基础上。
    这个其实很好懂,就像是平时排成绩表。先按照总分排名次,总分相同的再按照信息成绩(笑)排。这里也是一样,第二关键字的作用就是对于之前无法区分出来的继续进行区分。

    实现

    其实后缀数组的原理并不是很难,难点在于代码的实现。其实可以使用快排,但是复杂度会多个log。为了保证他的高效,一般都会使用桶排。
    具体实现见代码注释吧。真的挺绕挺难懂的。

    代码

    /*
    * @Author: wxyww
    * @Date:   2018-12-18 11:09:32
    * @Last Modified time: 2018-12-18 19:12:48
    */
    #include<cstdio>
    #include<iostream>
    #include<cstdlib>
    #include<cmath>
    #include<ctime>
    #include<cstring>
    #include<bitset>
    using namespace std;
    typedef long long ll;
    const int N = 1000000 + 100;
    ll read() {
       ll x=0,f=1;char c=getchar();
       while(c<'0'||c>'9') {
          if(c=='-') f=-1;
          c=getchar();
       }
       while(c>='0'&&c<='9') {
          x=x*10+c-'0';
          c=getchar();
       }
       return x*f;
    }
    int c[N],sa[N],x[N],y[N];
    //c为桶,sa[i]表示排名为i的元素的位置,x为当前数组的样子,y[i]为按照第二关键字排序排名为i的数字的第一关键字的位置
    int s[N];
    int n,m;
    void work() {
       for(int i = 1;i <= n;++i) ++c[x[i] = s[i]];//处理出c数组,并且把字符串赋值到x上
       for(int i = 2;i <= m;++i) c[i] += c[i - 1];//对c[i]做前缀和,来求出当前元素最靠后的排名,因为有重复的元素
       for(int i = n;i >= 1;--i) sa[c[x[i]]--] = i;//c[x[i]]--是因为使得每个排名都有元素,就先给他们人为安排一种顺序
       for(int k = 1;k <= n;k <<= 1) {
          int num = 0;
          for(int i = n - k + 1;i <= n;++i) y[++num] = i;//对于没有第二关键字的,他们的第二关键字是最靠前的
          for(int i = 1;i <= n;++i) if(sa[i] > k) y[++num] = sa[i] - k;//sa[i]储存的是单独这个元素的排名,所以只看第二关键字的话,就这么排咯
          for(int i = 1;i <= m;++i) c[i] = 0;
          for(int i = 1;i <= n;++i) ++c[x[i]];//还是处理出c数组
          for(int i = 2;i <= m;++i) c[i] += c[i - 1];
          //!!!结合第一关键字和第二关键字进行排名
          for(int i = n; i >= 1;--i) sa[c[x[y[i]]]--] = y[i];
          //这里要好好思考,y[i]表示第二关键字排名为i的元素的位置。x[y[i]]表示这个元素真正是多少,c[x[y[i]]]表示这种元素现在应该排多少,sa[c[x[y[i]]]]就表示这个排名所对应的元素
          //!!!
          for(int i = 1; i <= n;++i) y[i] = 0;
          swap(x,y);
          //把x数组清空,因为一会要按照新的排名重新给x赋值,并且把现在x的值赋给y,因为一会还要用
          x[sa[1]] = 1;
          num = 1;
          for(int i = 2;i <= n;++i) {
             if(y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) 
                x[sa[i]] = num;
             //如果通过当前关键字进行筛选之后,两个后缀还是无法区分出大小,那么就把他们赋值成一样大的,留给以后继续区分
             else x[sa[i]] = ++num;
          }
          if(num == n) break;
          m = num;
       }  
       for(int i = 1;i <= n;++i) printf("%d ",sa[i]);
    }
    int main() {
       // scanf("%s",s + 1);
       // n = strlen(s + 1);
       // m = 122;
       n = read();
       for(int i = 1;i <= n;++i) s[i] = read();
       m = 200;
       work();
       return 0;
    }
    

    LCP

    仅仅是对后缀进行排名,就显示不出后缀数组的强大了。他的另一个非常常用的作用就是求LCP。
    什么是LCP
    LCP是指最长公共前缀。也就是我们要对后缀求公共前缀。
    这有什么作用呢。对后缀求公共前缀,得到的就是一个字符串内的重复子串啊。这在后面的一些题中会有用处。
    一些性质
    这里我们用height[i]来表示排名为i的后缀与排名为i-1的后缀的LCP
    为了方便,我们在借用定义一些数组,rk[i]表示i这个后缀的排名(其实就是上面的名次数组),h[i]表示height[rk[i]] (似乎就只能这么表达了)。建议把这里搞清楚。
    性质一:(color {red}{h[i] >= h[i - 1] - 1})
    就是说,排名为i的后缀与排名为i-1的后缀的最长公共前缀大于等于排名为i-1的后缀与排名为i - 2的后缀 - 1.
    性质二:(color{red}{LCP(i,k) = min{ LCP(j,j - 1) }(i < j leq k)})
    也就是说,排名为i的后缀与排名为k的后缀的最长公共前缀,等于从i到k中所有串的最长公共前缀中最小的那个。
    利用性质一,我们可以求出来h数组和height数组,利用性质二,可以求出任意两个串的LCP。
    求LCP
    依据性质一,我们可以把h数组递推出来。直接看代码其实就很明了

    void get_height() {
       for(int i = 1;i <= n;++i) rk[sa[i]] = i;
       int k = 0;
       for(int i = 1;i <= n;++i) {
          if(rk[i] == 1) continue;
          if(k) --k;
          int j = sa[rk[i] - 1];
          while(j + k <= n && i + k <= n && a[j + k] == a[i + k]) ++k;
          height[rk[i]] = k;
       }
    }
    

    几道例题

    luogu3809
    只需要求出来sa数组就行了。
    bzoj1692
    题解
    bzoj1717
    题解
    POJ1743
    求不重叠的最长重复子串。
    二分一下答案,看是不是有重叠,就行了。
    代码(POJ1743)

    /*
    * @Author: wxyww
    * @Date:   2018-12-18 15:20:59
    * @Last Modified time: 2018-12-18 21:49:26
    */
    #include<cstdio>
    #include<iostream>
    #include<cstdlib>
    #include<cmath>
    #include<ctime>
    #include<bitset>
    using namespace std;
    typedef long long ll;
    const int N = 20000 + 100,INF = 1e8;
    ll read() {
       ll x=0,f=1;char c=getchar();
       while(c<'0'||c>'9') {
          if(c=='-') f=-1;
          c=getchar();
       }
       while(c>='0'&&c<='9') {
          x=x*10+c-'0';
          c=getchar();
       }
       return x*f;
    }
    int sa[N],rk[N],a[N],c[N],x[N],y[N];
    int height[N],h[N];
    int n,m;
    void get_sa() {
       for(int i = 1;i <= m;++i) c[i] = 0;
       for(int i = 1;i <= n;++i) ++c[x[i] = a[i]];
       for(int i = 2;i <= m;++i) c[i] += c[i - 1]; 
       for(int i = n;i >= 1;--i) sa[c[x[i]]--] = i;
       for(int k = 1;k <= n;k <<= 1) {
          int num = 0;
          for(int i = n - k + 1;i <= n; ++i) y[++num] = i;
          for(int i = 1;i <= n;++i) if(sa[i] > k) y[++num] = sa[i] - k;
          for(int i = 2;i <= m;++i) c[i] = 0;
          for(int i = 1;i <= n;++i) ++c[x[i]];
          for(int i = 1;i <= m;++i) c[i] += c[i - 1];
          for(int i = n;i >= 1;--i) sa[c[x[y[i]]]--] = y[i];
          swap(x,y);
          num = 0;
          x[sa[1]] = ++num;
          for(int i = 2;i <= n;++i) {
             if(y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) x[sa[i]] = num;
             else x[sa[i]] = ++num;
          }
          if(num == n) break;
          m = num;
       }
    }
    void get_height() {
       for(int i = 1;i <= n;++i) rk[sa[i]] = i;
       int k = 0;
       for(int i = 1;i <= n;++i) {
          if(rk[i] == 1) continue;
          if(k) --k;
          int j = sa[rk[i] - 1];
          while(j + k <= n && i + k <= n && a[j + k] == a[i + k]) ++k;
          height[rk[i]] = k;
       }
    }
    int check(int len) {
       int mn = INF,mx = -INF;
       for(int i = 1;i <= n;++i) {
          if(height[i] >= len) {
             mn = min(mn,min(sa[i],sa[i - 1]));
             mx = max(mx,max(sa[i],sa[i - 1]));
             if(mx - mn > len) return 1;
          }
          else mn = INF,mx = -INF;
       }
       return 0;
    }
    void erfen() {
       int ans = -1,l = 4,r = n;
       while(l <= r) {
          int mid = (l + r) >> 1;
          if(check(mid)) l = mid + 1,ans = mid;
          else r = mid - 1;
       }
        printf("%d
    ",ans + 1);
    }
    int main() {
       while(1) {
          n = read();
          m = -1;
          if(n == 0) break;
          for(int i = 1;i <= n;++i) a[i] = read();
          for(int i = 1;i < n;++i) a[i] = a[i + 1] - a[i] + 100;
          m = 200;
          get_sa();
    
          get_height();
          erfen();
       }
       return 0;
    }
    
    /*
    30
    25 27 30 34 39 45 52 60 69 79 69 60 52 45 39 34 30 26 22 18 82 78 74 70 66 67 64 60 65 80
    */
    
  • 相关阅读:
    Java微信二次开发(八)
    Java微信二次开发(七)
    Java微信二次开发(六)
    Java微信二次开发(五)
    Java微信二次开发(四)
    Java微信二次开发(三)
    IIS中X509Certificate遇见的问题
    SQL Server 数据库定时自动备份
    ASP.NET 5 (vNext) 理解和概述
    ASP.NET 5 (vNext) Linux部署
  • 原文地址:https://www.cnblogs.com/wxyww/p/10147331.html
Copyright © 2011-2022 走看看