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
    */
    
  • 相关阅读:
    5 Things Every Manager Should Know about Microsoft SharePoint 关于微软SharePoint每个经理应该知道的五件事
    Microsoft SharePoint 2010, is it a true Document Management System? 微软SharePoint 2010,它是真正的文档管理系统吗?
    You think you use SharePoint but you really don't 你认为你使用了SharePoint,但是实际上不是
    Introducing Document Management in SharePoint 2010 介绍SharePoint 2010中的文档管理
    Creating Your Own Document Management System With SharePoint 使用SharePoint创建你自己的文档管理系统
    MVP模式介绍
    权重初始化的选择
    机器学习中线性模型和非线性的区别
    神经网络激励函数的作用是什么
    深度学习中,交叉熵损失函数为什么优于均方差损失函数
  • 原文地址:https://www.cnblogs.com/wxyww/p/10147331.html
Copyright © 2011-2022 走看看