您的位置:首页 > 其它

学习小记——后缀数组

2017-03-15 22:21 260 查看

定义

在字符串处理当中,后缀树和后缀数组都是非常有力的工具,其中后缀树大家了解得比较多,关于后缀数组则很少见于国内的资料。其实后缀数组是后缀树的一个非常精巧的替代品,它比后缀树容易编程实现,能够实现后缀树的很多功能而时间复杂度也不太逊色,并且,它比后缀树所占用的空间小很多。可以说,在信息学竞赛中后缀数组比后缀树要更为实用。

变量定义

Suffix(i):表示S[i..len(S)],即从第i位开始的字符串后缀。

SA数组:后缀数组SA是一个一维数组,它保存1..n的某个排列SA[1],SA[2],……,SA
,并保证Suffix(SA[i])<Suffix(SA[i+1]),1≤i<n。也就是将S的n个后缀从小到大进行排序之后把排好序的后缀的开头位置顺次放入SA中。

rank数组:名次数组 Rank[i] 保存的是 Suffix(i) 在所有后缀中从小到大排列的“ 名次 ”后缀数组SA与名次数组Rank的对应关系。简单的说,后缀数组(SA)是“ 排第几的是谁?”,名次数组(RANK)是“ 你排第几?”。容易看出,后缀数组和名次数组为互逆运算。

height数组:height[i]=LCP(i-1,i),即第rank[i-1]与rank[i]的最长前缀是多长。

2倍增算法

比较容易实现的一种(相对于DC3算法,虽然DC3比较快,但是实现有点复杂,我不会……),一般的题目应该也够用了,时间复杂度O(n*log n),还算比较快了。

思路

我们倍增一个k,与RMQ的思想差不多,以2k−1的结果来推出2k,一步步倍增就能够实现求出所有的后缀的排名(具体定义详见《后缀数组——处理字符串的有力工具(罗穗骞)》)。(借了个图)



假设现在到了第三轮,k为2,第一关键字是上一轮同一位置的rank,第二关键字为上一轮向后k个位置的rank,然后进行双关键字排序(这里推荐用基数排序,当然快排也行,但是慢一点)。

memset(t,0,sizeof(t));
fo(i,1,n) t[rank[i+k]]++;
fo(i,1,da) t[i]+=t[i-1];
for(i=n;i>0;i--) se[t[rank[i+k]]--]=i;
//第二关键字排序
memset(t,0,sizeof(t));
fo(i,1,n) t[rank[i]]++;
fo(i,1,da) t[i]+=t[i-1];
for(i=n;i>0;i--) sa[t[rank[se[i]]]--]=se[i];
//第一关键字排序


就这样可以求出一个SA数组,但是数组之中都是不重复的,我们还要处理一下相同的情况。当S[i]=S[j]&&S[i+k]=S[j+k]时就定义为相同。

bool cmp(int *a,int x,int y,int z){return a[x]==a[y]&&a[x+z]==a[y+z];}

j=1;zs[sa[1]]=1;
fo(i,2,n)
zs[sa[i]]=cmp(rank,sa[i],sa[i-1],k)?j:++j;


求完了rank和SA数组后,就要求height数组,通过这个数组的一个特殊性质:h[i]≥h[i-1]-1,我们可以把复杂度降至O(n)。

i=j=k=0;
for(int i=1;i<=n;h[rank[i++]]=k)
for(k?k--:0,j=sa[rank[i]-1];a[i+k]==a[j+k];++k);


模板题

文件修复(JZOJ 1598)

//快速排序
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<math.h>
using namespace std;
#define fo(i,a,b) for(i=a;i<=b;i++)
const int N=1e5+5;
struct arr{
int x,y,bh;
}t
;
char s
;
int n,i,j,k,rank
,sa
,ws
,a
,h
,num,ans;
int cmp(arr x,arr y){return x.x<y.x||(x.x==y.x&&x.y<y.y);}
void deal(){
fo(i,1,n) rank[i]=a[i];
for(k=0;k<=n;){
fo(i,1,n){
t[i].x=rank[i],t[i].bh=i;
if(i+k>n) t[i].y=0;else t[i].y=rank[i+k];
}
sort(t+1,t+n+1,cmp);
rank[t[1].bh]=1;sa[1]=t[1].bh;num=1;
fo(i,2,n){
if(t[i-1].x!=t[i].x||t[i-1].y!=t[i].y) num++;
rank[t[i].bh]=num;
sa[i]=t[i].bh;
}
k*=2;
if(k==0) k=1;
}
i=j=k=0; for(int i=1;i<=n;h[rank[i++]]=k) for(k?k--:0,j=sa[rank[i]-1];a[i+k]==a[j+k];++k);
}
int main(){
scanf("%s",s);
n=strlen(s);
fo(i,1,n) a[i]=s[i-1];
deal();
ans=h[2];
fo(i,3,n) ans+=max(0,h[i]-h[i-1]);
printf("%d",ans);
}


还有一种快一点

//基数排序
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<math.h>
using namespace std;
#define fo(i,a,b) for(i=a;i<=b;i++)
const int N=1e5+5;
char s
;
int n,i,j,k,rank
,sa
,zs
,se
,t
,a
,h
,num,ans,da;
bool cmp(int *a,int x,int y,int z){return a[x]==a[y]&&a[x+z]==a[y+z];}
void deal(){
fo(i,1,n) rank[i]=a[i],se[i]=i;
for(k=0;k<=n;){
memset(t,0,sizeof(t));
fo(i,1,n) t[rank[i+k]]++;
fo(i,1,da) t[i]+=t[i-1];
for(i=n;i>0;i--) se[t[rank[i+k]]--]=i;

memset(t,0,sizeof(t));
fo(i,1,n) t[rank[i]]++;
fo(i,1,da) t[i]+=t[i-1];
for(i=n;i>0;i--) sa[t[rank[se[i]]]--]=se[i];
j=1;zs[sa[1]]=1;
fo(i,2,n)
zs[sa[i]]=cmp(rank,sa[i],sa[i-1],k)?j:++j;
swap(zs,rank);
if(k==0) k=1;else k*=2;
}
i=j=k=0; for(int i=1;i<=n;h[rank[i++]]=k) for(k?k--:0,j=sa[rank[i]-1];a[i+k]==a[j+k];++k);
}
int main(){
scanf("%s",s);
n=strlen(s);
fo(i,1,n) a[i]=s[i-1],da=max(da,a[i]);
da=max(n,1000);
deal();
ans=h[2];
fo(i,3,n) ans+=max(0,h[i]-h[i-1]);
printf("%d",ans);
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息