[后缀数组][学习笔记]

定义——来自百度百科

子串
一个字符串中连续的一段成为这个字符串的子串。
后缀
后缀是指从某个位置 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
*/
原文地址:https://www.cnblogs.com/wxyww/p/10147331.html