后缀数组

 先粘上我入门时看的博客:

https://www.cnblogs.com/jinkun113/p/4743694.html

https://www.cnblogs.com/victorique/p/8480093.html

https://www.cnblogs.com/jinkun113/p/4743694.html

声明:以下部分内容摘自该博客,仅供个人复习时用


后缀数组是让人蒙逼的有力工具! 后缀数组是处理字符串的有力工具,可以解决很多关于字符串的问题

 注意:后缀数组并不是一种算法,而是一种思想。

实现它的方法主要有两种:倍增法O(nlogn) 和 DC3法O(n)

其中倍增法除了仅仅在时间复杂度上不占优势之外,其他的方面例如编程难度,空间复杂度,常数等都秒杀DC3法

建议深入理解倍增法,并能熟练运用(起码8分钟内写出来&&没有错误)。DC3法只做了解。

什么是后缀数组

我们先看几条定义:

子串

在字符串s中,取任意i<=j,那么在s中截取从i到j的这一段就叫做s的一个子串

后缀

后缀就是从字符串的某个位置i到字符串末尾的子串,我们定义以s的第i个字符为第一个元素的后缀为suff(i)

后缀数组

把s的每个后缀按照字典序排序,

后缀数组sa[i]就表示排名为i的后缀的起始位置的下标

而它的映射数组rk[i]就表示起始位置的下标为i的后缀的排名

简单来说,sa表示排名为i的是啥,rk表示第i个的排名是啥

基数排序

  比较的复杂度为O(log n),如果这个时候再用快速排序的话,依旧需要O(n log^2 n),虽然已经小多了!但是,这个时候如果使用基数排序,可以进一步优化,达到O(n log n)。

  首先先来介绍这个以前没听过的排序方法。设存在一序列{73,22,93,43,55,14,28,65,39,81},首先根据个位数的数值,在遍历数据时将它们各自分配到编号0至9的桶(个位数值与桶号一一对应)中,如下图左侧所示:

  得到序列{81,22,73,93,43,14,55,65,28,39}。再根据十位数排序,如右侧,将他们连起来,得到序列{14,22,28,39,43,55,65,73,81,93}。

例题+板子:

https://www.luogu.org/problem/P3809

 1 #include <stdio.h>
 2 #include <string.h>
 3 #include <iostream>
 4 #include <string>
 5 #include <math.h>
 6 #include <algorithm>
 7 #include <vector>
 8 #include <stack>
 9 #include <queue>
10 #include <set>
11 #include <map>
12 #include <math.h>
13 const int INF=0x3f3f3f3f;
14 typedef long long LL;
15 const int mod=1e9+7;
16 const int maxn=1e6+10;
17 using namespace std;
18 
19 char str[maxn];
20 int sa[maxn];//排名为i的后缀的位置
21 int rk[maxn];//从第i个位置开始的后缀的排名
22 int C[maxn];//C数组表示桶,统计每种元素出现了多少次,辅助基数排序 
23 int X[maxn];//X[i]是第i个元素的第一关键字的字典排名 
24 int Y[maxn];//Y[i]表示在第二关键字中排名为i的数,其第一关键字的位置
25 
26 int n,m;
27 void Get_SA()
28 {
29     for(int i=1;i<=n;i++)
30         C[X[i]=str[i]]++;
31     for(int i=2;i<=m;i++)//做C的前缀和,我们就可以得出每个关键字最多是在第几名
32         C[i]+=C[i-1];
33     for(int i=n;i>=1;i--)
34         sa[C[X[i]]--] = i;
35     
36     for(int k = 1; k <= n; k <<= 1)
37     {//k:当前倍增的长度,k = x表示已经求出了长度为x的后缀的排名,现在要更新长度为2x的后缀的排名
38         int num=0; //num仅仅是一个计数器,示不同的后缀的个数,很显然原字符串的后缀都是不同的,因此num=n时可以退出循环
39         
40         for(int i=n-k+1;i<=n;i++)//第n-k+1到第n位是没有第二关键字的 所以排名在最前面
41             Y[++num]=i;
42         for(int i=1;i<=n;i++)
43             if(sa[i]>k)
44                 Y[++num]=sa[i]-k;
45 //排名为i的数 在数组中是否在第k位以后,即满足(sa[i]>k),那么它可以作为别人的第二关键字,就把它的第一关键字的位置添加进Y就行了
46 //所以i枚举的是第二关键字的排名,第二关键字靠前的先入队
47 
48         for(int i=1;i<=m;i++)
49             C[i]=0;//初始化C桶
50         for(int i=1;i<=n;i++)
51             C[X[Y[i]]]++;
52         //因为上一次循环已经算出了这次的第一关键字 所以直接加就行了
53         for(int i=1;i<=m;i++)
54             C[i]+=C[i-1];//第一关键字排名为1~i的数有多少个
55         for(int i=n;i>=1;i--)
56             sa[C[X[Y[i]]]--]=Y[i];
57 //因为Y的顺序是按照第二关键字的顺序来排的,第二关键字靠后的,在同一个第一关键字桶中排名越靠后
58 
59         swap(X,Y);//这里不用想太多,因为要生成新的X时要用到旧的,就把旧的复制下来,没别的意思
60         num=1;
61         X[sa[1]]=1;
62         for(int i=2;i<=n;i++)//因为sa[i]已经排好序了,所以可以按排名枚举,生成下一次的第一关键字X; 
63             X[sa[i]]=(Y[sa[i]]==Y[sa[i-1]] && Y[sa[i]+k]==Y[sa[i-1]+k]) ? num : ++num;
64         if(num==n)
65             break;
66         m=num;//这里就不用那个122了,因为都有新的编号了
67     }
68 }
69 
70 int main()
71 {
72     gets(1+str);
73     n=strlen(1+str);
74     m=122;
75     Get_SA();
76     for(int i=1;i<=n;i++)
77     {
78         if(i==1)
79             printf("%d",sa[i]);
80         else
81             printf(" %d",sa[i]);
82     }
83     printf("
");
84     return 0;
85 }

于是求出了所有后缀的排序,有什么用呢?主要是用于求它们之间的最长公共前缀(Longest Common Prefix,LCP)


最长公共前缀——后缀数组的辅助工具

什么是LCP?

我们定义LCP(i,j)为suff(sa[i])与suff(sa[j])的最长公共前缀

为什么要求LCP?

后缀数组这个东西,不可能只让你排个序就完事了……大多数情况下我们都需要用到这个辅助工具LCP来做题的

关于LCP的几条性质

显而易见的

  1. LCP(i,j)=LCP(j,i);
  2. LCP(i,i)=len(sa[i])=n-sa[i]+1;

这两条性质有什么用呢?对于i>j的情况,我们可以把它转化成i<j,对于i==j的情况,我们可以直接算长度,所以我们直接讨论i<j的情况就可以了。

我们每次依次比较字符肯定是不行的,单次复杂度为O(n),太高了,所以我们要做一定的预处理才行。

LCP Lemma

LCP(i,k)=min(LCP(i,j),LCP(j,k)) 对于任意1<=i<=j<=k<=n

证明:设p=min{LCP(i,j),LCP(j,k)},则有LCP(i,j)≥p,LCP(j,k)≥p。

设suff(sa[i])=u,suff(sa[j])=v,suff(sa[k])=w;

所以u和v的前p个字符相等,v和w的前p个字符相等

所以u和w的前p的字符相等,LCP(i,k)>=p

设LCP(i,k)=q>p 那么q>=p+1

因为p=min{LCP(i,j),LCP(j,k)},所以u[p+1]!=v[p+1] 或者 v[p+1]!=w[p+1]

但是u[p+1]=w[p+1] 这不就自相矛盾了吗

所以LCP(i,k)<=p

综上所述LCP(i,k)=p=min{LCP(i,j),LCP(j,k)}

LCP Theorem

LCP(i,k)=min(LCP(j,j-1)) 对于任意1<i<=j<=k<=n

这个结合LCP Lemma就很好理解了

我们可以把i~k拆成两部分i~(i+1)以及(i+1)~k

那么LCP(i,k)=min(LCP(i,i+1),LCP(i+1,k))

我们可以把(i+1)~k再拆,这样就像一个DP,正确性显然

怎么求LCP?

我们设height[i]为LCP(i,i-1) ,1<i<=n,显然height[1]=0;(height[i]表示sa[i-1]和sa[i]的最长公共前缀长度)

由LCP Theorem可得,LCP(i,k)=min(height[j]) i+1<=j<=k

那么height怎么求,枚举吗?NONONO,我们要利用这些后缀之间的联系

设h[i]=height[rk[i]],同样的,height[i]=h[sa[i]];

那么现在来证明最关键的一条定理:

h[i]>=h[i-1]-1;

证明过程来自曲神学长的blog,我做了一点改动方便初学者理解:

首先我们不妨设第i-1个字符串按排名来的前面的那个字符串是第k个字符串,注意k不一定是i-2,因为第k个字符串是按字典序排名来的i-1前面那个,并不是指在原字符串中位置在i-1前面的那个第i-2个字符串。

这时,依据height[]的定义,第k个字符串和第i-1个字符串的公共前缀自然是height[rk[i-1]],现在先讨论一下第k+1个字符串和第i个字符串的关系。

第一种情况,第k个字符串和第i-1个字符串的首字符不同,那么第k+1个字符串的排名既可能在i的前面,也可能在i的后面,但没有关系,因为height[rk[i-1]]就是0了呀,那么无论height[rk[i]]是多少都会有height[rk[i]]>=height[rk[i-1]]-1,也就是h[i]>=h[i-1]-1。

第二种情况,第k个字符串和第i-1个字符串的首字符相同,那么由于第k+1个字符串就是第k个字符串去掉首字符得到的,第i个字符串也是第i-1个字符串去掉首字符得到的,那么显然第k+1个字符串要排在第i个字符串前面。同时,第k个字符串和第i-1个字符串的最长公共前缀是height[rk[i-1]],

那么自然第k+1个字符串和第i个字符串的最长公共前缀就是height[rk[i-1]]-1。

到此为止,第二种情况的证明还没有完,我们可以试想一下,对于比第i个字符串的排名更靠前的那些字符串,谁和第i个字符串的相似度最高(这里说的相似度是指最长公共前缀的长度)?显然是排名紧邻第i个字符串的那个字符串了呀,即sa[rank[i]-1]。但是我们前面求得,有一个排在i前面的字符串k+1,LCP(rk[i],rk[k+1])=height[rk[i-1]]-1;

又因为height[rk[i]]=LCP(i,i-1)>=LCP(i,k+1)

所以height[rk[i]]>=height[rk[i-1]]-1,也即h[i]>=h[i-1]-1。

令LCP(i,j)为第i小的后缀和第j小的后缀(也就是Suffix(SA[i])和Suffix(SA[j]))的最长公共前缀的长度,则有如下两个性质:

  1. 对任意i<=k<=j,有LCP(i,j) = min(LCP(i,k),LCP(k,j))
  2. LCP(i,j)=min(i<k<=j)(LCP(k-1,k))

第一个性质是显然的,它的意义在于可以用来证明第二个性质。第二个性质的意义在于提供了一个将LCP问题转换为RMQ问题的方法:
令height[i]=LCP(i-1,i),即height[i]代表第i小的后缀与第i-1小的后缀的LCP,则求LCP(i,j)就等于求height[i+1]~height[j]之间的RMQ,套用RMQ算法就可以了,复杂度是预处理O(nlogn),查询O(1)

然后height的求法要用到另一个数组:令h[i]=height[SA[i]],即h[i]表示Suffix(i)的height值(同时height[i]就表示Suffix(SA[i])的height值),则有height[i]=h[Rank[i]]
然后h[i]有个性质:

  • h[i] >= h[i-1]-1

用这个性质我们在计算h[i]的时候进行后缀比较时只需从第h[i-1]位起比较,从而总的比较的复杂度是O(n),也就是说h数组在O(n)的时间内解决了。h解决了height也解决了,从而整个LCP问题就解决了^_^

然后后缀数组的应用就是利用它的LCP在需要字符串比较时降低复杂度。同时由于后缀数组的有序性可以很方便地使用二分

于是总结一下要点:

    • 利用倍增算法在O(nlogn)的时间内对后缀数组进行排序
    • 利用h数组的性质在O(n)的时间内求出排序后相邻后缀间的LCP数组height
    • 利用LCP的性质将平凡LCP问题转化为height数组上的RMQ问题
  1 #include <stdio.h>
  2 #include <string.h>
  3 #include <iostream>
  4 #include <string>
  5 #include <math.h>
  6 #include <algorithm>
  7 #include <vector>
  8 #include <stack>
  9 #include <queue>
 10 #include <set>
 11 #include <map>
 12 #include <math.h>
 13 const int INF=0x3f3f3f3f;
 14 typedef long long LL;
 15 const int mod=1e9+7;
 16 //const double PI=acos(-1);
 17 const int maxn=1e6+10;
 18 using namespace std;
 19 
 20 char str[maxn];
 21 int sa[maxn];//排名为i的后缀的位置
 22 int rk[maxn];//从第i个位置开始的后缀的排名
 23 int C[maxn];//C数组表示桶,统计每种元素出现了多少次,辅助基数排序 
 24 int X[maxn];//X[i]是第i个元素的第一关键字的字典排名 
 25 int Y[maxn];//Y[i]表示在第二关键字中排名为i的数,其第一关键字的位置
 26 int height[maxn];//height[i]表示排名为i和排名为i-1字符串的最长公共前缀长度 
 27 
 28 int n,m;
 29 void Get_sa()
 30 {
 31     for(int i=1;i<=n;i++)
 32         C[X[i]=str[i]]++;
 33     for(int i=2;i<=m;i++)//做C的前缀和,我们就可以得出每个关键字最多是在第几名
 34         C[i]+=C[i-1];
 35     for(int i=n;i>=1;i--)
 36         sa[C[X[i]]--] = i;
 37     
 38     for(int k = 1; k <= n; k <<= 1)
 39     {//k:当前倍增的长度,k = x表示已经求出了长度为x的后缀的排名,现在要更新长度为2x的后缀的排名
 40         int num=0; //num仅仅是一个计数器,示不同的后缀的个数,很显然原字符串的后缀都是不同的,因此num=n时可以退出循环
 41         
 42         for(int i=n-k+1;i<=n;i++)//第n-k+1到第n位是没有第二关键字的 所以排名在最前面
 43             Y[++num]=i;
 44         for(int i=1;i<=n;i++)
 45             if(sa[i]>k)
 46                 Y[++num]=sa[i]-k;
 47 //排名为i的数 在数组中是否在第k位以后,即满足(sa[i]>k),那么它可以作为别人的第二关键字,就把它的第一关键字的位置添加进Y就行了
 48 //所以i枚举的是第二关键字的排名,第二关键字靠前的先入队
 49 
 50         for(int i=1;i<=m;i++)
 51             C[i]=0;//初始化C桶
 52         for(int i=1;i<=n;i++)
 53             C[X[Y[i]]]++;
 54         //因为上一次循环已经算出了这次的第一关键字 所以直接加就行了
 55         for(int i=1;i<=m;i++)
 56             C[i]+=C[i-1];//第一关键字排名为1~i的数有多少个
 57         for(int i=n;i>=1;i--)
 58             sa[C[X[Y[i]]]--]=Y[i];
 59 //因为Y的顺序是按照第二关键字的顺序来排的,第二关键字靠后的,在同一个第一关键字桶中排名越靠后
 60 
 61         swap(X,Y);//这里不用想太多,因为要生成新的X时要用到旧的,就把旧的复制下来,没别的意思
 62         num=1;
 63         X[sa[1]]=1;
 64         for(int i=2;i<=n;i++)//因为sa[i]已经排好序了,所以可以按排名枚举,生成下一次的第一关键字X; 
 65             X[sa[i]]=(Y[sa[i]]==Y[sa[i-1]] && Y[sa[i]+k]==Y[sa[i-1]+k]) ? num : ++num;
 66         if(num==n)
 67             break;
 68         m=num;//这里就不用那个122了,因为都有新的编号了
 69     }
 70 }
 71 
 72 void Get_height()
 73 {
 74     int k=0;
 75     for(int i=1;i<=n;i++)//求出rk 
 76         rk[sa[i]]=i;
 77 
 78     for(int i=1;i<=n;i++)
 79     {
 80         if(rk[i]==1)//第一名height为0
 81             continue;
 82         if(k)//h[i]>=h[i-1]-1;
 83             k--;
 84         int j=sa[rk[i]-1];
 85         while(j+k<=n&&i+k<=n&&str[i+k]==str[j+k])
 86             k++;
 87         height[rk[i]]=k;//h[i]=height[rk[i]];
 88     }    
 89 }
 90 
 91 int main()
 92 {
 93     gets(1+str);
 94     n=strlen(1+str);
 95     m=122;//n表示原字符串长度,m表示字符个数,ASCII('z')=122,我们第一次读入字符直接不用转化,按原来的ASCII码来就可以了
 96     Get_sa();
 97     Get_height();
 98     printf("sa:");
 99     for(int i=1;i<=n;i++)
100     {
101         printf("%d",sa[i]);
102         if(i==n)
103             printf("
");
104     }
105     printf("rk:");
106     for(int i=1;i<=n;i++)
107     {
108         printf("%d",rk[i]);
109         if(i==n)
110             printf("
");
111     }
112     printf("height:");
113     for(int i=1;i<=n;i++)
114     {
115         printf("%d",height[i]);
116         if(i==n)
117             printf("
");
118     }
119     return 0;
120 }

经典应用

两个后缀的最大公共前缀

lcp(x,y)=min(heigh[xy]), 用rmq维护,O(1)查询

可重叠最长重复子串

Height数组里的最大值

不可重叠最长重复子串 POJ1743

首先二分答案x,对height数组进行分组,保证每一组的minheight>=x>=x

依次枚举每一组,记录下最大和最小长度,多sa[mx]sa[mi]>=x那么可以更新答案

本质不同的子串的数量

枚举每一个后缀,第i个后缀对答案的贡献为lensa[i]+1height[i]

4个比较基础的应用
Q1:一个串中两个串的最大公共前缀是多少?
A1:这不就是Height吗?用rmq预处理,再O(1)查询。

Q2:一个串中可重叠的重复最长子串是多长?
A2:就是求任意两个后缀的最长公共前缀,而任意两个后缀的最长公共前缀都是Height 数组里某一段的最小值,那最长的就是Height中的最大值。

Q3:一个串种不可重叠的重复最长子串是多长?
A3:先二分答案,转化成判别式的问题比较好处理。假设当前需要判别长度为k是否符合要求,只需把排序后的后缀分成若干组,其中每组的后缀之间的Height 值都不小于k,再判断其中有没有不重复的后缀,具体就是看最大的SA值和最小的SA值相差超不超过k,有一组超过的话k就是合法答案。

A4:一个字符串不相等的子串的个数是多少?
Q4:每个子串一定是某个后缀的前缀,那么原问题等价于求所有后缀之间的不相同的前缀的个数。而且可以发现每一个后缀Suffix[SA[i]]的贡献是Len - SA[i] + 1,但是有子串算重复,重复的就是Heigh[i]个与前面相同的前缀,那么减去就可以了。最后,一个后缀Suffix[SA[i]]的贡献就是Len - SA[k] + 1 - Height[k]。
对于后缀数组更多的应用这里就不详细阐述,经过思考后每个人都会发现它的一些不同的用途,它的功能也许比你想象中的更强大!


 声明:我博客里有大量的从别的博客复制过来的代码,分析,以及理解,但我一律会在文章后面标记原博客大佬博客地址,其中部分会加以连接。 绝无抄袭的意思,只是为了我在复习的时候找博客方便。如有原作者对此有不满,请在博客留言,我一定会删除该博文。

原文地址:https://www.cnblogs.com/jiamian/p/11503808.html