【SPOJ705】New Distinct Substrings
Description
Given a string, we need to find the total number of its distinct substrings.
Input
The test case consists of one string, whose length is <= 200000
Output
For the test case output one number saying the number of distinct substrings.
Sample Input
ABABA
Sample Output
9
Solution
蒟蒻第一次接触到神奇的后缀数组。
这道题的题意简明易懂,求一个字符串本质不同的子串个数,即仅为位置不同但字符相同的子串记作一个。如果不知道后缀数组或是其他的字符串处理算法的话,是很难直接构造出一个有效的模型来研究这种数据的。但是我们拥有后缀数组这一有力的工具,所以我们可以较为轻松的得出思路。(代码第一次敲很难看就是了)
我们令suffix(i)代表第i个位置的后缀,而后缀数组就是要得出这n个后缀的排序以及位置。我们据此构造出两个相关联的数组,分别记为Sa[i]和rank[i]。其中Sa[i]代表的是排名第i的后缀的起始位置,而rank[i]则是起始位置为i的后缀的排名。通过这一定义,我们不难得出这样两个个式子:rank[Sa[i]]=i;Sa[rank[i]]=i;
上述两个式子证明了Sa[i]和rank[i]两个数组之间的关联性,即只要得出其中任何一个,另外一个就可以在O(n)的时间内转移。那么我们现在的任务就是求出Sa[i]和rank[i]。
如果我们直接用sort之类的快排方式来解决排序的话,由于字符串比较的时间复杂度为O(n),那么总的时间复杂度就会十分接近于O(n^2),这显然是我们不能接受的,所以我们要找到一种合理的排序法。O(n)的排序很明显是没有的,看上去这个方法就无解了?
在上述的排序过程中,各种快速排序的时间复杂度都没有O(n^2)这样让人不能接受。我们不能接受的是比较字符串所带来的额外的O(n)复杂度。由此看来,我们不能使用基于元素比较的排序方式。
接下来我们就需要介绍一下一种独特的排序方式了:计数排序
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k),其中k为整数的范围。我们可以将一个字符串视为数字来进行排序,由于字符的范围较小,所以这一方法可以近似的认为是在O(n)的时间内得出排序。
虽然说这种方式只能够快速得出单个字符的大小关系,但是我们完全可以用倍增的思想来解决全部的排序。这个方法的证明本蒟蒻写不出来(懒得画图),就只好让各位看看代码或者是移步大牛们的博客了。通过这样的排序之后,我们终于构造出了Sa[i]和rank[i]这两个数组用于准备。除此之外,还有一个height[i]数组是后缀数组的重要组成部分。
在水过height[i]数组之前,我们先来水一下LCP(i,j)的概念。在这里,LCP(i,j)指的是suffix(i)和suffix(j)的最长公共前缀。说起来有些奇怪,但是我们接下来确实是要去求出同一个字符串的不同后缀之间的最长公共前缀。很好理解的是,一个字符串的任一子串是可以由该串的后缀的前缀来表示的。在此,结合上面提到的rank[i]数组的含义,我们可以推导出LCP(i,j)的求法。即LCP(i,j)=min{LCP(k,k-1)}(i<=k<=j)。
我们的height[i]表示的就是LCP(Sa[i-1],Sa[i])。这样我们就可以通过RMQ或是SegTree之类的区间询问算法来得出LCP(i,j),进而解决有关字符串的许多问题了。为了在线性时间内完成对height[i]数组的计算,所以我们定义h[i]=height[rank[i]]。即suffix(i)与排名在它前一位的后缀的最长公共前缀长度。
听说h[i]还有一个性质h[i]>=h[i-1]-1,蒟蒻确实没证过。但有了这个性质,我们就可以根据h[1],h[2],…,h[n]的顺序先计算出h[i]。再根据height[i]=h[sa[i]]计算出height[i]数组。总的时间复杂度可以证明不超过O(4*n),所以height[i]数组的线性构造也证明了。
这道题的题意简明易懂,求一个字符串本质不同的子串个数,即仅为位置不同但字符相同的子串记作一个。如果不知道后缀数组或是其他的字符串处理算法的话,是很难直接构造出一个有效的模型来研究这种数据的。但是我们拥有后缀数组这一有力的工具,所以我们可以较为轻松的得出思路。(代码第一次敲很难看就是了)
我们令suffix(i)代表第i个位置的后缀,而后缀数组就是要得出这n个后缀的排序以及位置。我们据此构造出两个相关联的数组,分别记为Sa[i]和rank[i]。其中Sa[i]代表的是排名第i的后缀的起始位置,而rank[i]则是起始位置为i的后缀的排名。通过这一定义,我们不难得出这样两个个式子:rank[Sa[i]]=i;Sa[rank[i]]=i;
上述两个式子证明了Sa[i]和rank[i]两个数组之间的关联性,即只要得出其中任何一个,另外一个就可以在O(n)的时间内转移。那么我们现在的任务就是求出Sa[i]和rank[i]。
如果我们直接用sort之类的快排方式来解决排序的话,由于字符串比较的时间复杂度为O(n),那么总的时间复杂度就会十分接近于O(n^2),这显然是我们不能接受的,所以我们要找到一种合理的排序法。O(n)的排序很明显是没有的,看上去这个方法就无解了?
在上述的排序过程中,各种快速排序的时间复杂度都没有O(n^2)这样让人不能接受。我们不能接受的是比较字符串所带来的额外的O(n)复杂度。由此看来,我们不能使用基于元素比较的排序方式。
接下来我们就需要介绍一下一种独特的排序方式了:计数排序
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k),其中k为整数的范围。我们可以将一个字符串视为数字来进行排序,由于字符的范围较小,所以这一方法可以近似的认为是在O(n)的时间内得出排序。
虽然说这种方式只能够快速得出单个字符的大小关系,但是我们完全可以用倍增的思想来解决全部的排序。这个方法的证明本蒟蒻写不出来(懒得画图),就只好让各位看看代码或者是移步大牛们的博客了。通过这样的排序之后,我们终于构造出了Sa[i]和rank[i]这两个数组用于准备。除此之外,还有一个height[i]数组是后缀数组的重要组成部分。
在水过height[i]数组之前,我们先来水一下LCP(i,j)的概念。在这里,LCP(i,j)指的是suffix(i)和suffix(j)的最长公共前缀。说起来有些奇怪,但是我们接下来确实是要去求出同一个字符串的不同后缀之间的最长公共前缀。很好理解的是,一个字符串的任一子串是可以由该串的后缀的前缀来表示的。在此,结合上面提到的rank[i]数组的含义,我们可以推导出LCP(i,j)的求法。即LCP(i,j)=min{LCP(k,k-1)}(i<=k<=j)。
我们的height[i]表示的就是LCP(Sa[i-1],Sa[i])。这样我们就可以通过RMQ或是SegTree之类的区间询问算法来得出LCP(i,j),进而解决有关字符串的许多问题了。为了在线性时间内完成对height[i]数组的计算,所以我们定义h[i]=height[rank[i]]。即suffix(i)与排名在它前一位的后缀的最长公共前缀长度。
听说h[i]还有一个性质h[i]>=h[i-1]-1,蒟蒻确实没证过。但有了这个性质,我们就可以根据h[1],h[2],…,h[n]的顺序先计算出h[i]。再根据height[i]=h[sa[i]]计算出height[i]数组。总的时间复杂度可以证明不超过O(4*n),所以height[i]数组的线性构造也证明了。
CODE
将上述的数组费心费力的算出来后,ans累加n-Sa[i]+1-height[i]就好了。
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
namespace Suffix_Array {//学习一下新的写法,命名空间里的内容不会和主程序中的命名冲突。
const int N=400005;
char S[N];
int Sa[N],rank[N],a[N],b[N],sum[N],*x,*y,H[N],n;
//用指针来实现Sa和rank在倍增过程中的交替迭代
int i,j,k,p;
void Init(){gets(S+1);n=strlen(S+1);x=a;y=b;return ;}//神奇的输入,貌似有空格。。。
void Radix(int m){//排序
memset(sum,0,sizeof(sum));
for(i=1;i<=n;i++)sum[x[y[i]]]++;
for(i=1;i<=m;i++)sum[i]+=sum[i-1];
for(i=n;i>=1;i--)Sa[sum[x[y[i]]]--]=y[i];
return ;
}
void Make_Sa(int m){
for(int i=1;i<=n;i++)x[i]=S[i],y[i]=i;//用赋值代替new,给指针定向
Radix(m);//单字符排序
for(k=1,p=0;k<=n;k<<=1,m=p,p=0){
for(i=n-k+1;i<=n;i++)y[++p]=i;
for(i=1;i<=n;i++)if(Sa[i]>k)y[++p]=Sa[i]-k;//添加前一次排序的排名
Radix(m);swap(x,y);x[Sa[1]]=p=1;//重新排序,交换两次的数组,将上一次的结果x,本次结果交给y
for(i=2;i<=n;i++)x[Sa[i]]=(y[Sa[i]]==y[Sa[i-1]])&&(y[Sa[i]+k]==y[Sa[i-1]+k])?p:++p;
//重新倍增排名,若当前比较的字符串的前一半已经得出结果或者后一半可以得出结果,那么该串的排名增加,否则与上一名相同
if(p==n)break ;
}for(i=1;i<=n;i++)rank[Sa[i]]=i;//rank与Sa的相互转换
return ;
}
void Make_Height(){//求出height[i]数组
memset(H,0,sizeof(H));
for(i=1,k=0;i<=n;H[rank[i++]]=k)
for(k?k--:1;i+k<=n&&S[i+k]==S[Sa[rank[i]-1]+k];k++);
return ;
}
}long long ans=0;
int main() {
using namespace Suffix_Array;//应用名字空间
Init();Make_Sa(128);Make_Height();//这次字符大小最多到128
for(int i=1;i<=n;i++)ans+=n-Sa[i]+1-H[i];//累计答案
cout<<ans;
return 0;
}